From 77ca88fd3a38107f8a673dba1efc3fc0a4504516 Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Thu, 4 May 2017 05:54:05 -0700 Subject: [PATCH 01/46] Add ext-grpc as a dep for services that only provides grpc lib (#475) --- src/ErrorReporting/composer.json | 1 + src/Monitoring/composer.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/ErrorReporting/composer.json b/src/ErrorReporting/composer.json index a0a4427d3faf..fbd0a494d34a 100644 --- a/src/ErrorReporting/composer.json +++ b/src/ErrorReporting/composer.json @@ -4,6 +4,7 @@ "license": "Apache-2.0", "minimum-stability": "stable", "require": { + "ext-grpc": "*", "google/cloud-core": "^1.0", "google/proto-client-php": "^0.9", "google/gax": "^0.8" diff --git a/src/Monitoring/composer.json b/src/Monitoring/composer.json index 0ee3edf7cf84..b67dc35fe88e 100644 --- a/src/Monitoring/composer.json +++ b/src/Monitoring/composer.json @@ -4,6 +4,7 @@ "license": "Apache-2.0", "minimum-stability": "stable", "require": { + "ext-grpc": "*", "google/cloud-core": "^1.0", "google/proto-client-php": "^0.9", "google/gax": "^0.8" From 942a0c4c25ef2b4a2317c82e4e0b8e50f8e2ad6f Mon Sep 17 00:00:00 2001 From: Takashi Matsuo Date: Thu, 4 May 2017 10:20:52 -0700 Subject: [PATCH 02/46] Choose rest over grpc for now. (#477) --- src/Core/ClientTrait.php | 11 +++++------ src/Logging/LoggingClient.php | 3 +-- src/PubSub/PubSubClient.php | 3 +-- tests/unit/Core/ClientTraitTest.php | 7 ++++++- tests/unit/PubSub/PubSubClientTest.php | 7 ++----- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Core/ClientTrait.php b/src/Core/ClientTrait.php index 4bf1083816d9..bdfe971e552e 100644 --- a/src/Core/ClientTrait.php +++ b/src/Core/ClientTrait.php @@ -38,8 +38,8 @@ trait ClientTrait private $projectId; /** - * Get either a gRPC or REST connection based on the provided config - * and system settings. + * Get either a gRPC or REST connection based on the provided config. + * * * @param array $config * @return string @@ -47,13 +47,12 @@ trait ClientTrait */ private function getConnectionType(array $config) { - list($isGrpcExtensionLoaded, $isGrpcLibraryLoaded) = $this->getGrpcDependencyStatus(); - $defaultTransport = $isGrpcExtensionLoaded && $isGrpcLibraryLoaded ? 'grpc' : 'rest'; $transport = isset($config['transport']) ? strtolower($config['transport']) - : $defaultTransport; - + : 'rest'; if ($transport === 'grpc') { + list($isGrpcExtensionLoaded, $isGrpcLibraryLoaded) = + $this->getGrpcDependencyStatus(); if (!$isGrpcExtensionLoaded || !$isGrpcLibraryLoaded) { throw new GoogleException( 'gRPC support has been requested but required dependencies ' . diff --git a/src/Logging/LoggingClient.php b/src/Logging/LoggingClient.php index 1c5169a8f38c..503e15122109 100644 --- a/src/Logging/LoggingClient.php +++ b/src/Logging/LoggingClient.php @@ -108,8 +108,7 @@ class LoggingClient * **Defaults to** `3`. * @type array $scopes Scopes to be used for the request. * @type string $transport The transport type used for requests. May be - * either `grpc` or `rest`. **Defaults to** `grpc` if gRPC support - * is detected on the system. + * either `grpc` or `rest`. **Defaults to** `rest`. * } */ public function __construct(array $config = []) diff --git a/src/PubSub/PubSubClient.php b/src/PubSub/PubSubClient.php index 299f06e803dc..a78f5e161f48 100644 --- a/src/PubSub/PubSubClient.php +++ b/src/PubSub/PubSubClient.php @@ -124,8 +124,7 @@ class PubSubClient * **Defaults to** `3`. * @type array $scopes Scopes to be used for the request. * @type string $transport The transport type used for requests. May be - * either `grpc` or `rest`. **Defaults to** `grpc` if gRPC support - * is detected on the system. + * either `grpc` or `rest`. **Defaults to** `rest`. * } * @throws \InvalidArgumentException */ diff --git a/tests/unit/Core/ClientTraitTest.php b/tests/unit/Core/ClientTraitTest.php index d79f04120472..255c8eb23bbf 100644 --- a/tests/unit/Core/ClientTraitTest.php +++ b/tests/unit/Core/ClientTraitTest.php @@ -52,7 +52,7 @@ public function dependencyStatusProvider() [ [true, true], [], - 'grpc' + 'rest' ], [ [false, false], @@ -63,6 +63,11 @@ public function dependencyStatusProvider() [false, true], [], 'rest' + ], + [ + [true, true], + ['transport' => 'grpc'], + 'grpc' ] ]; } diff --git a/tests/unit/PubSub/PubSubClientTest.php b/tests/unit/PubSub/PubSubClientTest.php index 1f1d9b7c7251..fc93adac4c2e 100644 --- a/tests/unit/PubSub/PubSubClientTest.php +++ b/tests/unit/PubSub/PubSubClientTest.php @@ -49,14 +49,11 @@ public function setUp() ]); } - public function testUsesGrpcConnectionByDefault() + public function testUsesRestConnectionByDefault() { - if (!extension_loaded('grpc')) { - $this->markTestSkipped('Must have the grpc extension installed to run this test.'); - } $client = new PubSubClientStub(['projectId' => 'project']); - $this->assertInstanceOf(Grpc::class, $client->getConnection()); + $this->assertInstanceOf(Rest::class, $client->getConnection()); } public function testCreateTopic() From 7d17699a0b18fddcc6e3d1daf8b9363d22e72a09 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 11 May 2017 19:31:05 -0400 Subject: [PATCH 03/46] project to projectId (#484) --- src/BigQuery/BigQueryClient.php | 2 +- tests/unit/BigQuery/BigQueryClientTest.php | 34 ++++++++++++---------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/BigQuery/BigQueryClient.php b/src/BigQuery/BigQueryClient.php index 7714e3aabdd6..521297a80ae6 100644 --- a/src/BigQuery/BigQueryClient.php +++ b/src/BigQuery/BigQueryClient.php @@ -381,7 +381,7 @@ function (array $job) { ); }, [$this->connection, 'listJobs'], - $options + ['project' => $this->projectId], + $options + ['projectId' => $this->projectId], [ 'itemsKey' => 'jobs', 'resultLimit' => $resultLimit diff --git a/tests/unit/BigQuery/BigQueryClientTest.php b/tests/unit/BigQuery/BigQueryClientTest.php index b4030ff4f374..190691fffd9a 100644 --- a/tests/unit/BigQuery/BigQueryClientTest.php +++ b/tests/unit/BigQuery/BigQueryClientTest.php @@ -167,7 +167,7 @@ public function testGetsJob() public function testGetsJobsWithNoResults() { - $this->connection->listJobs(Argument::any()) + $this->connection->listJobs(['projectId' => $this->projectId]) ->willReturn([]) ->shouldBeCalledTimes(1); @@ -179,7 +179,7 @@ public function testGetsJobsWithNoResults() public function testGetsJobsWithoutToken() { - $this->connection->listJobs(Argument::any()) + $this->connection->listJobs(['projectId' => $this->projectId]) ->willReturn([ 'jobs' => [ ['jobReference' => ['jobId' => $this->jobId]] @@ -195,21 +195,23 @@ public function testGetsJobsWithoutToken() public function testGetsJobsWithToken() { - $this->connection->listJobs(Argument::any()) - ->willReturn( - [ - 'nextPageToken' => 'token', - 'jobs' => [ - ['jobReference' => ['jobId' => 'someOtherJobId']] - ] - ], - [ - 'jobs' => [ - ['jobReference' => ['jobId' => $this->jobId]] - ] + $token = 'token'; + $this->connection->listJobs(['projectId' => $this->projectId]) + ->willReturn([ + 'nextPageToken' => $token, + 'jobs' => [ + ['jobReference' => ['jobId' => 'someOtherJobId']] ] - ) - ->shouldBeCalledTimes(2); + ])->shouldBeCalledTimes(1); + $this->connection->listJobs([ + 'projectId' => $this->projectId, + 'pageToken' => $token + ]) + ->willReturn([ + 'jobs' => [ + ['jobReference' => ['jobId' => $this->jobId]] + ] + ])->shouldBeCalledTimes(1); $this->client->setConnection($this->connection->reveal()); $job = iterator_to_array($this->client->jobs()); From e3c800580ae964a676a33c649e600ceb0370e777 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Fri, 12 May 2017 08:14:57 -0700 Subject: [PATCH 04/46] RFC: Adds removePolicy to Iam Policy Builder (#480) * Adds removePolicy to Iam Policy Builder * adds snippet test --- src/Core/Iam/PolicyBuilder.php | 47 ++++++++++ tests/snippets/Core/Iam/PolicyBuilderTest.php | 10 +++ tests/unit/Core/Iam/PolicyBuilderTest.php | 87 +++++++++++++++++++ 3 files changed, 144 insertions(+) diff --git a/src/Core/Iam/PolicyBuilder.php b/src/Core/Iam/PolicyBuilder.php index 86fbba4b445a..cabb885b5a72 100644 --- a/src/Core/Iam/PolicyBuilder.php +++ b/src/Core/Iam/PolicyBuilder.php @@ -123,6 +123,53 @@ public function addBinding($role, array $members) return $this; } + /** + * Remove a binding from the policy. + * + * Example: + * ``` + * $builder->setBindings([ + * [ + * 'role' => 'roles/admin', + * 'members' => [ + * 'user:admin@domain.com', + * 'user2:admin@domain.com' + * ] + * ] + * ]); + * $builder->removeBinding('roles/admin', [ 'user:admin@domain.com' ]); + * ``` + * + * @param string $role A valid role for the service + * @param array $members An array of members to remove from the role + * @return PolicyBuilder + * @throws InvalidArgumentException + */ + public function removeBinding($role, array $members) + { + $bindings = $this->bindings; + foreach ((array) $bindings as $i => $binding) { + if ($binding['role'] == $role) { + $newMembers = array_diff($binding['members'], $members); + if (count($newMembers) != count($binding['members']) - count($members)) { + throw new InvalidArgumentException('One or more role-members were not found.'); + } + if (empty($newMembers)) { + unset($bindings[$i]); + $bindings = array_values($bindings); + } else { + $binding['members'] = array_values($newMembers); + $bindings[$i] = $binding; + } + $this->bindings = $bindings; + + return $this; + } + } + + throw new InvalidArgumentException('The role was not found.'); + } + /** * Update the etag on the policy. * diff --git a/tests/snippets/Core/Iam/PolicyBuilderTest.php b/tests/snippets/Core/Iam/PolicyBuilderTest.php index 32bf8bb19d71..546d128f94ee 100644 --- a/tests/snippets/Core/Iam/PolicyBuilderTest.php +++ b/tests/snippets/Core/Iam/PolicyBuilderTest.php @@ -62,6 +62,16 @@ public function testAddBindings() $this->assertEquals('user:admin@domain.com', $this->pb->result()['bindings'][0]['members'][0]); } + public function testRemoveBinding() + { + $snippet = $this->snippetFromMethod(PolicyBuilder::class, 'removeBinding'); + $snippet->addLocal('builder', $this->pb); + + $res = $snippet->invoke(); + $this->assertEquals('roles/admin', $this->pb->result()['bindings'][0]['role']); + $this->assertEquals('user2:admin@domain.com', $this->pb->result()['bindings'][0]['members'][0]); + } + public function testSetEtag() { $snippet = $this->snippetFromMethod(PolicyBuilder::class, 'setEtag'); diff --git a/tests/unit/Core/Iam/PolicyBuilderTest.php b/tests/unit/Core/Iam/PolicyBuilderTest.php index 3b3c3ec6a40d..b2764a4ea96e 100644 --- a/tests/unit/Core/Iam/PolicyBuilderTest.php +++ b/tests/unit/Core/Iam/PolicyBuilderTest.php @@ -137,4 +137,91 @@ public function testConstructWithExistingPolicy() $this->assertEquals($policy, $result); } + + public function testRemoveBinding() + { + $policy = [ + 'bindings' => [ + [ + 'role' => 'test', + 'members' => [ + 'user:test@test.com', + 'user2:test@test.com' + ] + ] + ] + ]; + + $builder = new PolicyBuilder($policy); + $builder->removeBinding('test', ['user:test@test.com']); + + $this->assertEquals('user2:test@test.com', $builder->result()['bindings'][0]['members'][0]); + } + + public function testRemoveBindingAndRole() + { + $policy = [ + 'bindings' => [ + [ + 'role' => 'test', + 'members' => [ + 'user:test@test.com', + ] + ], + [ + 'role' => 'test2', + 'members' => [ + 'user2:test@test.com' + ] + ] + ] + ]; + + $builder = new PolicyBuilder($policy); + $builder->removeBinding('test', ['user:test@test.com']); + + $this->assertEquals('user2:test@test.com', $builder->result()['bindings'][0]['members'][0]); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage One or more role-members were not found. + */ + public function testRemoveBindingInvalidMemberThrowsException() + { + $policy = [ + 'bindings' => [ + [ + 'role' => 'test', + 'members' => [ + 'user:test@test.com', + ] + ], + ] + ]; + + $builder = new PolicyBuilder($policy); + $builder->removeBinding('test', ['user2:test@test.com']); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage The role was not found. + */ + public function testRemoveBindingInvalidRoleThrowsException() + { + $policy = [ + 'bindings' => [ + [ + 'role' => 'test', + 'members' => [ + 'user:test@test.com', + ] + ], + ] + ]; + + $builder = new PolicyBuilder($policy); + $builder->removeBinding('test2', ['user:test@test.com']); + } } From 462629f460c8539b0f186bf3fc7e9d3f7e97a443 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 15 May 2017 17:57:14 -0400 Subject: [PATCH 05/46] Introduce support for Cloud Spanner (#333) --- composer.json | 3 +- dev/src/Functions.php | 29 + dev/src/Snippet/SnippetTestCase.php | 6 +- dev/src/StubTrait.php | 55 + docs/contents/cloud-spanner.json | 8 + phpunit.xml.dist | 2 + src/Core/ArrayTrait.php | 17 + src/Core/Exception/AbortedException.php | 47 + .../Exception/FailedPreconditionException.php | 27 + src/Core/Exception/ServiceException.php | 14 +- src/Core/GrpcRequestWrapper.php | 44 +- src/Core/GrpcTrait.php | 2 +- src/Core/LongRunning/LROTrait.php | 45 + .../LongRunningConnectionInterface.php | 46 + src/Core/LongRunning/LongRunningOperation.php | 377 +++++ .../LongRunning/OperationResponseTrait.php | 102 ++ src/Core/PhpArray.php | 7 +- src/Core/RequestWrapper.php | 4 + src/Core/Retry.php | 106 ++ src/Logging/Connection/Grpc.php | 20 +- src/ServiceBuilder.php | 27 + .../Admin/Database/V1/DatabaseAdminClient.php | 1049 ++++++++++++++ .../database_admin_client_config.json | 73 + .../Admin/Instance/V1/InstanceAdminClient.php | 1243 +++++++++++++++++ .../instance_admin_client_config.json | 78 ++ src/Spanner/Bytes.php | 111 ++ src/Spanner/Configuration.php | 194 +++ .../Connection/ConnectionInterface.php | 174 +++ src/Spanner/Connection/Grpc.php | 647 +++++++++ src/Spanner/Connection/IamDatabase.php | 63 + src/Spanner/Connection/IamInstance.php | 63 + .../Connection/LongRunningConnection.php | 72 + src/Spanner/Database.php | 1122 +++++++++++++++ src/Spanner/Date.php | 110 ++ src/Spanner/Duration.php | 121 ++ src/Spanner/Instance.php | 472 +++++++ src/Spanner/KeyRange.php | 237 ++++ src/Spanner/KeySet.php | 239 ++++ src/Spanner/LICENSE | 202 +++ src/Spanner/Operation.php | 382 +++++ src/Spanner/README.md | 16 + src/Spanner/Result.php | 203 +++ src/Spanner/Session/Session.php | 145 ++ src/Spanner/Session/SessionClient.php | 104 ++ src/Spanner/Session/SessionPool.php | 63 + .../Spanner/Session/SessionPoolInterface.php | 18 +- src/Spanner/Session/SimpleSessionPool.php | 54 + src/Spanner/Snapshot.php | 146 ++ src/Spanner/SpannerClient.php | 550 ++++++++ src/Spanner/Timestamp.php | 129 ++ src/Spanner/Transaction.php | 465 ++++++ src/Spanner/TransactionConfigurationTrait.php | 155 ++ src/Spanner/TransactionReadTrait.php | 108 ++ src/Spanner/V1/SpannerClient.php | 1180 ++++++++++++++++ .../V1/resources/spanner_client_config.json | 78 ++ src/Spanner/ValueInterface.php | 44 + src/Spanner/ValueMapper.php | 324 +++++ src/Spanner/composer.json | 26 + .../snippets/BigQuery/BigQueryClientTest.php | 32 +- tests/snippets/BigQuery/QueryResultsTest.php | 10 +- tests/snippets/BigQuery/TableTest.php | 39 +- tests/snippets/BigQuery/TimeTest.php | 3 +- tests/snippets/BigQuery/TimestampTest.php | 3 +- tests/snippets/Core/Iam/IamTest.php | 12 +- .../LongRunning/LongRunningOperationTest.php | 301 ++++ .../snippets/Datastore/Query/GqlQueryTest.php | 9 +- tests/snippets/Datastore/Query/QueryTest.php | 7 +- tests/snippets/Datastore/TransactionTest.php | 24 +- .../snippets/Language/LanguageClientTest.php | 14 +- tests/snippets/Logging/LoggerTest.php | 18 +- tests/snippets/Logging/LoggingClientTest.php | 14 +- tests/snippets/Logging/MetricTest.php | 14 +- tests/snippets/Logging/SinkTest.php | 16 +- tests/snippets/PubSub/PubSubClientTest.php | 14 +- tests/snippets/PubSub/SubscriptionTest.php | 30 +- tests/snippets/PubSub/TopicTest.php | 31 +- tests/snippets/ServiceBuilderTest.php | 2 + tests/snippets/Spanner/BytesTest.php | 81 ++ tests/snippets/Spanner/ConfigurationTest.php | 122 ++ tests/snippets/Spanner/DatabaseTest.php | 713 ++++++++++ tests/snippets/Spanner/DateTest.php | 78 ++ tests/snippets/Spanner/DurationTest.php | 80 ++ tests/snippets/Spanner/InstanceTest.php | 222 +++ tests/snippets/Spanner/KeyRangeTest.php | 95 ++ tests/snippets/Spanner/KeySetTest.php | 134 ++ tests/snippets/Spanner/SnapshotTest.php | 167 +++ tests/snippets/Spanner/SpannerClientTest.php | 273 ++++ tests/snippets/Spanner/TimestampTest.php | 79 ++ tests/snippets/Spanner/TransactionTest.php | 330 +++++ tests/snippets/Speech/OperationTest.php | 8 +- tests/snippets/Speech/SpeechClientTest.php | 12 +- tests/snippets/Storage/AclTest.php | 14 +- tests/snippets/Storage/BucketTest.php | 31 +- tests/snippets/Storage/StorageClientTest.php | 14 +- tests/snippets/Storage/StorageObjectTest.php | 32 +- .../Translate/TranslateClientTest.php | 16 +- tests/snippets/Vision/VisionClientTest.php | 8 +- tests/snippets/bootstrap.php | 32 - tests/system/Spanner/AdminTest.php | 102 ++ tests/system/Spanner/ConfigurationTest.php | 53 + tests/system/Spanner/OperationsTest.php | 142 ++ tests/system/Spanner/SnapshotTest.php | 57 + tests/system/Spanner/SpannerTestCase.php | 80 ++ tests/system/Spanner/TransactionTest.php | 46 + tests/system/bootstrap.php | 8 +- tests/unit/Core/ArrayTraitTest.php | 20 + tests/unit/Core/PhpArrayTest.php | 2 +- tests/unit/Datastore/OperationTest.php | 5 + tests/unit/Spanner/BytesTest.php | 52 + tests/unit/Spanner/ConfigurationTest.php | 116 ++ .../Spanner/Connection/IamDatabaseTest.php | 66 + .../Spanner/Connection/IamInstanceTest.php | 66 + .../Connection/LongRunningConnectionTest.php | 66 + tests/unit/Spanner/DatabaseTest.php | 620 ++++++++ tests/unit/Spanner/DateTest.php | 55 + tests/unit/Spanner/DurationTest.php | 65 + tests/unit/Spanner/InstanceTest.php | 318 +++++ tests/unit/Spanner/KeyRangeTest.php | 95 ++ tests/unit/Spanner/KeySetTest.php | 119 ++ tests/unit/Spanner/OperationTest.php | 303 ++++ tests/unit/Spanner/ResultTest.php | 106 ++ tests/unit/Spanner/SnapshotTest.php | 48 + tests/unit/Spanner/SpannerClientTest.php | 288 ++++ tests/unit/Spanner/TimestampTest.php | 61 + .../TransactionConfigurationTraitTest.php | 185 +++ tests/unit/Spanner/TransactionTest.php | 324 +++++ tests/unit/Spanner/ValueMapperTest.php | 385 +++++ tests/unit/fixtures/spanner/instance.json | 7 + 128 files changed, 18180 insertions(+), 262 deletions(-) create mode 100644 dev/src/Functions.php create mode 100644 dev/src/StubTrait.php create mode 100644 docs/contents/cloud-spanner.json create mode 100644 src/Core/Exception/AbortedException.php create mode 100644 src/Core/Exception/FailedPreconditionException.php create mode 100644 src/Core/LongRunning/LROTrait.php create mode 100644 src/Core/LongRunning/LongRunningConnectionInterface.php create mode 100644 src/Core/LongRunning/LongRunningOperation.php create mode 100644 src/Core/LongRunning/OperationResponseTrait.php create mode 100644 src/Core/Retry.php create mode 100644 src/Spanner/Admin/Database/V1/DatabaseAdminClient.php create mode 100644 src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json create mode 100644 src/Spanner/Admin/Instance/V1/InstanceAdminClient.php create mode 100644 src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json create mode 100644 src/Spanner/Bytes.php create mode 100644 src/Spanner/Configuration.php create mode 100644 src/Spanner/Connection/ConnectionInterface.php create mode 100644 src/Spanner/Connection/Grpc.php create mode 100644 src/Spanner/Connection/IamDatabase.php create mode 100644 src/Spanner/Connection/IamInstance.php create mode 100644 src/Spanner/Connection/LongRunningConnection.php create mode 100644 src/Spanner/Database.php create mode 100644 src/Spanner/Date.php create mode 100644 src/Spanner/Duration.php create mode 100644 src/Spanner/Instance.php create mode 100644 src/Spanner/KeyRange.php create mode 100644 src/Spanner/KeySet.php create mode 100644 src/Spanner/LICENSE create mode 100644 src/Spanner/Operation.php create mode 100644 src/Spanner/README.md create mode 100644 src/Spanner/Result.php create mode 100644 src/Spanner/Session/Session.php create mode 100644 src/Spanner/Session/SessionClient.php create mode 100644 src/Spanner/Session/SessionPool.php rename dev/src/SetStubConnectionTrait.php => src/Spanner/Session/SessionPoolInterface.php (66%) create mode 100644 src/Spanner/Session/SimpleSessionPool.php create mode 100644 src/Spanner/Snapshot.php create mode 100644 src/Spanner/SpannerClient.php create mode 100644 src/Spanner/Timestamp.php create mode 100644 src/Spanner/Transaction.php create mode 100644 src/Spanner/TransactionConfigurationTrait.php create mode 100644 src/Spanner/TransactionReadTrait.php create mode 100644 src/Spanner/V1/SpannerClient.php create mode 100644 src/Spanner/V1/resources/spanner_client_config.json create mode 100644 src/Spanner/ValueInterface.php create mode 100644 src/Spanner/ValueMapper.php create mode 100644 src/Spanner/composer.json create mode 100644 tests/snippets/Core/LongRunning/LongRunningOperationTest.php create mode 100644 tests/snippets/Spanner/BytesTest.php create mode 100644 tests/snippets/Spanner/ConfigurationTest.php create mode 100644 tests/snippets/Spanner/DatabaseTest.php create mode 100644 tests/snippets/Spanner/DateTest.php create mode 100644 tests/snippets/Spanner/DurationTest.php create mode 100644 tests/snippets/Spanner/InstanceTest.php create mode 100644 tests/snippets/Spanner/KeyRangeTest.php create mode 100644 tests/snippets/Spanner/KeySetTest.php create mode 100644 tests/snippets/Spanner/SnapshotTest.php create mode 100644 tests/snippets/Spanner/SpannerClientTest.php create mode 100644 tests/snippets/Spanner/TimestampTest.php create mode 100644 tests/snippets/Spanner/TransactionTest.php create mode 100644 tests/system/Spanner/AdminTest.php create mode 100644 tests/system/Spanner/ConfigurationTest.php create mode 100644 tests/system/Spanner/OperationsTest.php create mode 100644 tests/system/Spanner/SnapshotTest.php create mode 100644 tests/system/Spanner/SpannerTestCase.php create mode 100644 tests/system/Spanner/TransactionTest.php create mode 100644 tests/unit/Spanner/BytesTest.php create mode 100644 tests/unit/Spanner/ConfigurationTest.php create mode 100644 tests/unit/Spanner/Connection/IamDatabaseTest.php create mode 100644 tests/unit/Spanner/Connection/IamInstanceTest.php create mode 100644 tests/unit/Spanner/Connection/LongRunningConnectionTest.php create mode 100644 tests/unit/Spanner/DatabaseTest.php create mode 100644 tests/unit/Spanner/DateTest.php create mode 100644 tests/unit/Spanner/DurationTest.php create mode 100644 tests/unit/Spanner/InstanceTest.php create mode 100644 tests/unit/Spanner/KeyRangeTest.php create mode 100644 tests/unit/Spanner/KeySetTest.php create mode 100644 tests/unit/Spanner/OperationTest.php create mode 100644 tests/unit/Spanner/ResultTest.php create mode 100644 tests/unit/Spanner/SnapshotTest.php create mode 100644 tests/unit/Spanner/SpannerClientTest.php create mode 100644 tests/unit/Spanner/TimestampTest.php create mode 100644 tests/unit/Spanner/TransactionConfigurationTraitTest.php create mode 100644 tests/unit/Spanner/TransactionTest.php create mode 100644 tests/unit/Spanner/ValueMapperTest.php create mode 100644 tests/unit/fixtures/spanner/instance.json diff --git a/composer.json b/composer.json index 38de7429e585..4731d19473da 100644 --- a/composer.json +++ b/composer.json @@ -71,7 +71,8 @@ "psr-4": { "Google\\Cloud\\Dev\\": "dev/src", "Google\\Cloud\\Tests\\System\\": "tests/system" - } + }, + "files": ["dev/src/Functions.php"] }, "scripts": { "google-cloud": "dev/google-cloud" diff --git a/dev/src/Functions.php b/dev/src/Functions.php new file mode 100644 index 000000000000..fea2e9cf8d88 --- /dev/null +++ b/dev/src/Functions.php @@ -0,0 +1,29 @@ +newInstanceArgs($args); +} diff --git a/dev/src/Snippet/SnippetTestCase.php b/dev/src/Snippet/SnippetTestCase.php index 1bbf1e9ce293..b07a1deafc95 100644 --- a/dev/src/Snippet/SnippetTestCase.php +++ b/dev/src/Snippet/SnippetTestCase.php @@ -53,7 +53,7 @@ public function snippetFromClass($class, $indexOrName = 0) self::$coverage->cover($snippet->identifier()); - return $snippet; + return clone $snippet; } /** @@ -77,7 +77,7 @@ public function snippetFromMagicMethod($class, $method, $indexOrName = 0) self::$coverage->cover($identifier); - return $snippet; + return clone $snippet; } /** @@ -100,6 +100,6 @@ public function snippetFromMethod($class, $method, $indexOrName = 0) self::$coverage->cover($identifier); - return $snippet; + return clone $snippet; } } diff --git a/dev/src/StubTrait.php b/dev/src/StubTrait.php new file mode 100644 index 000000000000..7a710ed8bb72 --- /dev/null +++ b/dev/src/StubTrait.php @@ -0,0 +1,55 @@ +___getPropertyReflector($prop); + + $property->setAccessible(true); + return $property->getValue($this); + } + + public function ___setProperty($prop, $value) + { + if (!in_array($prop, json_decode($this->___props))) { + throw new \BadMethodCallException(sprintf('Property %s cannot be overloaded', $prop)); + } + + $property = $this->___getPropertyReflector($prop); + + $property->setAccessible(true); + $property->setValue($this, $value); + } + + private function ___getPropertyReflector($property) + { + $trait = new \ReflectionClass($this); + $ref = $trait->getParentClass(); + + try { + $property = $ref->getProperty($property); + } catch (\ReflectionException $e) { + throw new \BadMethodCallException($e->getMessage()); + } + + return $property; + } +} diff --git a/docs/contents/cloud-spanner.json b/docs/contents/cloud-spanner.json new file mode 100644 index 000000000000..191f68771ba9 --- /dev/null +++ b/docs/contents/cloud-spanner.json @@ -0,0 +1,8 @@ +{ + "title": "Spanner", + "pattern": "spanner\/\\w{1,}", + "nav": [{ + "title": "SpannerClient", + "type": "spanner/spannerclient" + }] +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8afd82d909fa..23718b678755 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,6 +10,8 @@ src src/*/V[!a-zA-Z]* + src/*/*/V[!a-zA-Z]* + src/*/*/*/V[!a-zA-Z]* diff --git a/src/Core/ArrayTrait.php b/src/Core/ArrayTrait.php index 402507de987d..efca6404bf27 100644 --- a/src/Core/ArrayTrait.php +++ b/src/Core/ArrayTrait.php @@ -78,4 +78,21 @@ private function isAssoc(array $arr) { return array_keys($arr) !== range(0, count($arr) - 1); } + + /** + * Just like array_filter(), but preserves falsey values except null. + * + * @param array $arr + * @return array + */ + private function arrayFilterRemoveNull(array $arr) + { + return array_filter($arr, function ($element) { + if (!is_null($element)) { + return true; + } + + return false; + }); + } } diff --git a/src/Core/Exception/AbortedException.php b/src/Core/Exception/AbortedException.php new file mode 100644 index 000000000000..60cd19e02130 --- /dev/null +++ b/src/Core/Exception/AbortedException.php @@ -0,0 +1,47 @@ +options, function ($metadataItem) { + if (array_key_exists('retryDelay', $metadataItem)) { + return true; + } + + return false; + }); + + $delay = $metadata[0]['retryDelay']; + if (!isset($delay['seconds'])) { + $delay['seconds'] = 0; + } + + return $delay; + } +} diff --git a/src/Core/Exception/FailedPreconditionException.php b/src/Core/Exception/FailedPreconditionException.php new file mode 100644 index 000000000000..5c001c431e06 --- /dev/null +++ b/src/Core/Exception/FailedPreconditionException.php @@ -0,0 +1,27 @@ +serviceException = $serviceException; + $this->options = $options; parent::__construct($message, $code); } diff --git a/src/Core/GrpcRequestWrapper.php b/src/Core/GrpcRequestWrapper.php index f8de0d63c22f..ad42cf7d85ea 100644 --- a/src/Core/GrpcRequestWrapper.php +++ b/src/Core/GrpcRequestWrapper.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Core; +use DrSlump\Protobuf\Codec\Binary; use DrSlump\Protobuf\Codec\CodecInterface; use DrSlump\Protobuf\Message; use Google\Auth\FetchAuthTokenInterface; @@ -25,6 +26,7 @@ use Google\Cloud\Core\PhpArray; use Google\Cloud\Core\RequestWrapperTrait; use Google\GAX\ApiException; +use Google\GAX\OperationResponse; use Google\GAX\PagedListResponse; use Google\GAX\RetrySettings; use Grpc; @@ -47,6 +49,11 @@ class GrpcRequestWrapper */ private $codec; + /** + * @var CodecInterface A codec used for binary deserialization. + */ + private $binaryCodec; + /** * @var array gRPC specific configuration options passed off to the GAX * library. @@ -63,6 +70,13 @@ class GrpcRequestWrapper Grpc\STATUS_DATA_LOSS ]; + /** + * @var array Map of error metadata types to RPC wrappers. + */ + private $metadataTypes = [ + 'google.rpc.retryinfo-bin' => \google\rpc\RetryInfo::class + ]; + /** * @param array $config [optional] { * Configuration options. Please see @@ -88,6 +102,7 @@ public function __construct(array $config = []) $this->authHttpHandler = $config['authHttpHandler'] ?: HttpHandlerFactory::build(); $this->codec = $config['codec']; $this->grpcOptions = $config['grpcOptions']; + $this->binaryCodec = new Binary; } /** @@ -134,7 +149,7 @@ public function send(callable $request, array $args, array $options = []) try { return $this->handleResponse($backoff->execute($request, $args)); - } catch (\Exception $ex) { + } catch (ApiException $ex) { throw $this->convertToGoogleException($ex); } } @@ -155,6 +170,10 @@ private function handleResponse($response) return $response->serialize($this->codec); } + if ($response instanceof OperationResponse) { + return $response; + } + return null; } @@ -179,6 +198,10 @@ private function convertToGoogleException(ApiException $ex) $exception = Exception\ConflictException::class; break; + case Grpc\STATUS_FAILED_PRECONDITION: + $exception = Exception\FailedPreconditionException::class; + break; + case Grpc\STATUS_UNKNOWN: $exception = Exception\ServerException::class; break; @@ -187,11 +210,28 @@ private function convertToGoogleException(ApiException $ex) $exception = Exception\ServerException::class; break; + case Grpc\STATUS_ABORTED: + $exception = Exception\AbortedException::class; + break; + default: $exception = Exception\ServiceException::class; break; } - return new $exception($ex->getMessage(), $ex->getCode(), $ex); + $metadata = []; + if ($ex->getMetadata()) { + foreach ($ex->getMetadata() as $type => $binaryValue) { + if (!isset($this->metadataTypes[$type])) { + continue; + } + + $metadata[] = (new $this->metadataTypes[$type]) + ->deserialize($binaryValue[0], $this->binaryCodec) + ->serialize($this->codec); + } + } + + return new $exception($ex->getMessage(), $ex->getCode(), $ex, $metadata); } } diff --git a/src/Core/GrpcTrait.php b/src/Core/GrpcTrait.php index fd9cfb0a8c8a..212f527c2e67 100644 --- a/src/Core/GrpcTrait.php +++ b/src/Core/GrpcTrait.php @@ -191,7 +191,7 @@ private function formatTimestampForApi($value) preg_match('/\.(\d{1,9})Z/', $value, $matches); $value = preg_replace('/\.(\d{1,9})Z/', '.000000Z', $value); $dt = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', $value); - $nanos = (isset($matches[1])) ? str_pad($matches[1], 9, '0') : 0; + $nanos = (isset($matches[1])) ? $matches[1] : 0; return [ 'seconds' => (int)$dt->format('U'), diff --git a/src/Core/LongRunning/LROTrait.php b/src/Core/LongRunning/LROTrait.php new file mode 100644 index 000000000000..0b965357231e --- /dev/null +++ b/src/Core/LongRunning/LROTrait.php @@ -0,0 +1,45 @@ +connection = $connection; + $this->name = $name; + $this->callablesMap = $callablesMap; + } + + /** + * Return the Operation name. + * + * Example: + * ``` + * $name = $operation->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Check if the Operation is done. + * + * If the Operation state is not available, a service request may be executed + * by this method. + * + * Example: + * ``` + * if ($operation->done()) { + * echo "The operation is done!"; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return bool + */ + public function done(array $options = []) + { + return (isset($this->info($options)['done'])) + ? $this->info['done'] + : false; + } + + /** + * Get the state of the Operation. + * + * Return value will be one of `LongRunningOperation::STATE_IN_PROGRESS`, + * `LongRunningOperation::STATE_SUCCESS` or + * `LongRunningOperation::STATE_ERROR`. + * + * If the Operation state is not available, a service request may be executed + * by this method. + * + * Example: + * ``` + * switch ($operation->state()) { + * case LongRunningOperation::STATE_IN_PROGRESS: + * echo "Operation is in progress"; + * break; + * + * case LongRunningOperation::STATE_SUCCESS: + * echo "Operation succeeded"; + * break; + * + * case LongRunningOperation::STATE_ERROR: + * echo "Operation failed"; + * break; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return string + */ + public function state(array $options = []) + { + if (!$this->done($options)) { + return self::STATE_IN_PROGRESS; + } + + if ($this->done() && $this->result()) { + return self::STATE_SUCCESS; + } + + return self::STATE_ERROR; + } + + /** + * Get the Operation result. + * + * The return type of this method is dictated by the type of Operation. + * + * Returns null if the Operation is not yet complete, or if an error occurred. + * + * If the Operation state is not available, a service request may be executed + * by this method. + * + * Example: + * ``` + * $result = $operation->result(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return mixed|null + */ + public function result(array $options = []) + { + $this->info($options); + return $this->result; + } + + /** + * Get the Operation error. + * + * Returns null if the Operation is not yet complete, or if no error occurred. + * + * If the Operation state is not available, a service request may be executed + * by this method. + * + * Example: + * ``` + * $error = $operation->error(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return array|null + */ + public function error(array $options = []) + { + $this->info($options); + return $this->error; + } + + /** + * Get the Operation info. + * + * If the Operation state is not available, a service request may be executed + * by this method. + * + * Example: + * ``` + * $info = $operation->info(); + * ``` + * + * @codingStandardsIgnoreStart + * @param array $options [optional] Configuration options. + * @return array [google.longrunning.Operation](https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operation) + * @codingStandardsIgnoreEnd + */ + public function info(array $options = []) + { + return $this->info ?: $this->reload($options); + } + + /** + * Reload the Operation to check its status. + * + * Example: + * ``` + * $result = $operation->reload(); + * ``` + * + * @codingStandardsIgnoreStart + * @param array $options [optional] Configuration Options. + * @return array [google.longrunning.Operation](https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operation) + * @codingStandardsIgnoreEnd + */ + public function reload(array $options = []) + { + $res = $this->connection->get([ + 'name' => $this->name, + ] + $options); + + $this->result = null; + $this->error = null; + if (isset($res['done']) && $res['done']) { + $type = $res['metadata']['typeUrl']; + $this->result = $this->executeDoneCallback($type, $res['response']); + $this->error = (isset($res['error'])) + ? $res['error'] + : null; + } + + return $this->info = $res; + } + + /** + * Reload the operation until it is complete. + * + * The return type of this method is dictated by the type of Operation. If + * `$options.maxPollingDurationSeconds` is set, and the poll exceeds the + * limit, the return will be `null`. + * + * Example: + * ``` + * $result = $operation->pollUntilComplete(); + * ``` + * + * @param array $options { + * Configuration Options + * + * @type float $pollingIntervalSeconds The polling interval to use, in + * seconds. **Defaults to** `1.0`. + * @type float $maxPollingDurationSeconds The maximum amount of time to + * continue polling. **Defaults to** `0.0`. + * } + * @return mixed|null + */ + public function pollUntilComplete(array $options = []) + { + $options += [ + 'pollingIntervalSeconds' => $this::WAIT_INTERVAL, + 'maxPollingDurationSeconds' => 0.0, + ]; + + $pollingIntervalMicros = $options['pollingIntervalSeconds'] * 1000000; + $maxPollingDuration = $options['maxPollingDurationSeconds']; + $hasMaxPollingDuration = $maxPollingDuration > 0.0; + $endTime = microtime(true) + $maxPollingDuration; + + do { + usleep($pollingIntervalMicros); + $this->reload($options); + } while (!$this->done() && (!$hasMaxPollingDuration || microtime(true) < $endTime)); + + return $this->result; + } + + /** + * Cancel a Long Running Operation. + * + * Example: + * ``` + * $operation->cancel(); + * ``` + * + * @param array $options Configuration options. + * @return void + */ + public function cancel(array $options = []) + { + $this->connection->cancel([ + 'name' => $this->name + ]); + } + + /** + * Delete a Long Running Operation. + * + * Example: + * ``` + * $operation->delete(); + * ``` + * + * @param array $options Configuration Options. + * @return void + */ + public function delete(array $options = []) + { + $this->connection->delete([ + 'name' => $this->name + ]); + } + + /** + * When the Operation is complete, there may be a callback enqueued to + * handle the response. If so, execute it and return the result. + * + * @param string $type The response type. + * @param mixed $response The response data. + * @return mixed + */ + private function executeDoneCallback($type, $response) + { + if (is_null($response)) { + return null; + } + + $callables = array_filter($this->callablesMap, function ($callable) use ($type) { + return $callable['typeUrl'] === $type; + }); + + if (count($callables) === 0) { + return $response; + } + + $callable = current($callables); + $fn = $callable['callable']; + + return call_user_func($fn, $response); + } + + /** + * @access private + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'name' => $this->name, + 'callablesMap' => array_keys($this->callablesMap) + ]; + } +} diff --git a/src/Core/LongRunning/OperationResponseTrait.php b/src/Core/LongRunning/OperationResponseTrait.php new file mode 100644 index 000000000000..233f7a2122c7 --- /dev/null +++ b/src/Core/LongRunning/OperationResponseTrait.php @@ -0,0 +1,102 @@ +getLastProtoResponse(); + if (is_null($response)) { + return null; + } + + $response = $response->serialize($codec); + + $result = null; + if ($operation->isDone()) { + $type = $response['metadata']['typeUrl']; + $result = $this->deserializeResult($operation, $type, $codec, $lroMappers); + } + + $error = $operation->getError(); + if (!is_null($error)) { + $error = $error->serialize($codec); + } + + $response['response'] = $result; + $response['error'] = $error; + + return $response; + } + + /** + * Fetch an OperationResponse object from a gapic client. + * + * @param mixed $client A generated client with a `resumeOperation` method. + * @param string $name The Operation name. + * @return OperationResponse + */ + private function getOperationByName($client, $name) + { + return $client->resumeOperation($name); + } + + /** + * Convert an operation response to an array + * + * @param OperationResponse $operation The operation to serialize. + * @param string $type The Operation type. The type should correspond to a + * member of $mappers.typeUrl. + * @param CodecInterface $codec The gRPC codec to use for the deserialization. + * @param array $mappers A list of mappers. + * @return array|null + */ + private function deserializeResult(OperationResponse $operation, $type, CodecInterface $codec, array $mappers) + { + $mappers = array_filter($mappers, function ($mapper) use ($type) { + return $mapper['typeUrl'] === $type; + }); + + if (count($mappers) === 0) { + throw new \RuntimeException(sprintf('No mapper exists for operation response type %s.', $type)); + } + + $mapper = current($mappers); + $message = $mapper['message']; + + $response = new $message(); + $anyResponse = $operation->getLastProtoResponse()->getResponse(); + + if (is_null($anyResponse)) { + return null; + } + + $response->parse($anyResponse->getValue()); + + return $response->serialize($codec); + } +} diff --git a/src/Core/PhpArray.php b/src/Core/PhpArray.php index 4210f63bdd1b..a18ae19f0872 100644 --- a/src/Core/PhpArray.php +++ b/src/Core/PhpArray.php @@ -18,8 +18,9 @@ namespace Google\Cloud\Core; use DrSlump\Protobuf; -use google\protobuf\Struct; use google\protobuf\ListValue; +use google\protobuf\NullValue; +use google\protobuf\Struct; /** * Extend the Protobuf-PHP array codec to allow messages to match the format @@ -156,6 +157,10 @@ protected function decodeMessage(Protobuf\Message $message, $data) protected function filterValue($value, Protobuf\Field $field) { + if (trim($field->getReference(), '\\') === NullValue::class) { + return null; + } + if ($value instanceof Protobuf\Message) { if ($this->isKeyValueMessage($value)) { $v = $value->getValue(); diff --git a/src/Core/RequestWrapper.php b/src/Core/RequestWrapper.php index c8e55bfe1f4a..2418a8a1bde9 100644 --- a/src/Core/RequestWrapper.php +++ b/src/Core/RequestWrapper.php @@ -225,6 +225,10 @@ private function convertToGoogleException(\Exception $ex) $exception = Exception\ConflictException::class; break; + case 412: + $exception = Exception\FailedPreconditionException::class; + break; + case 500: $exception = Exception\ServerException::class; break; diff --git a/src/Core/Retry.php b/src/Core/Retry.php new file mode 100644 index 000000000000..ebcae9fb786e --- /dev/null +++ b/src/Core/Retry.php @@ -0,0 +1,106 @@ + (int >= 0), 'nanos' => (int >= 0)] specifying how + * long an operation should pause before retrying. Should accept a + * single argument of type `\Exception`. + * @param callable $retryFunction [optional] returns bool for whether or not + * to retry. + */ + public function __construct( + $retries, + callable $delayFunction, + callable $retryFunction = null + ) { + $this->retries = $retries !== null ? (int) $retries : 3; + $this->retryFunction = $retryFunction; + $this->delayFunction = $delayFunction; + } + + /** + * Executes the retry process. + * + * @param callable $function + * @param array $arguments [optional] + * @return mixed + * @throws \Exception The last exception caught while retrying. + */ + public function execute(callable $function, array $arguments = []) + { + $delayFunction = $this->delayFunction; + $retryAttempt = 0; + $exception = null; + + while (true) { + try { + return call_user_func_array($function, $arguments); + } catch (\Exception $exception) { + if ($this->retryFunction) { + if (!call_user_func($this->retryFunction, $exception)) { + throw $exception; + } + } + + if ($retryAttempt >= $this->retries) { + break; + } + + $delayFunction($exception); + $retryAttempt++; + } + } + + throw $exception; + } + + /** + * @param callable $delayFunction + * @return void + */ + public function setDelayFunction(callable $delayFunction) + { + $this->delayFunction = $delayFunction; + } +} diff --git a/src/Logging/Connection/Grpc.php b/src/Logging/Connection/Grpc.php index b5f00f8aeb94..cfa006bbb0f4 100644 --- a/src/Logging/Connection/Grpc.php +++ b/src/Logging/Connection/Grpc.php @@ -90,15 +90,17 @@ class Grpc implements ConnectionInterface public function __construct(array $config = []) { $this->codec = new PhpArray([ - 'timestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - }, - 'severity' => function ($v) { - return Logger::getLogLevelMap()[$v]; - }, - 'outputVersionFormat' => function ($v) { - return self::$versionFormatMap[$v]; - } + 'customFilters' => [ + 'timestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + }, + 'severity' => function ($v) { + return Logger::getLogLevelMap()[$v]; + }, + 'outputVersionFormat' => function ($v) { + return self::$versionFormatMap[$v]; + } + ] ]); $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index bcbed5c9f11d..ad0541e685c4 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -23,6 +23,7 @@ use Google\Cloud\Logging\LoggingClient; use Google\Cloud\Language\LanguageClient; use Google\Cloud\PubSub\PubSubClient; +use Google\Cloud\Spanner\SpannerClient; use Google\Cloud\Speech\SpeechClient; use Google\Cloud\Storage\StorageClient; use Google\Cloud\Trace\TraceClient; @@ -114,6 +115,7 @@ public function __construct(array $config = []) * @type bool $returnInt64AsObject If true, 64 bit integers will be * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit * platform compatibility. **Defaults to** false. + * } * @return BigQueryClient */ public function bigQuery(array $config = []) @@ -210,6 +212,31 @@ public function pubsub(array $config = []) return new PubSubClient($config ? $this->resolveConfig($config) : $this->config); } + /** + * Google Cloud Spanner is a highly scalable, transactional, managed, NewSQL + * database service. Find more information at + * [Google Cloud Spanner API docs](https://cloud.google.com/spanner/). + * + * Example: + * ``` + * $spanner = $cloud->spanner(); + * ``` + * + * @param array $config [optional] { + * Configuration options. See + * {@see Google\Cloud\ServiceBuilder::__construct()} for the other available options. + * + * @type bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit + * platform compatibility. **Defaults to** false. + * } + * @return SpannerClient + */ + public function spanner(array $config = []) + { + return new SpannerClient($config ? $this->resolveConfig($config) : $this->config); + } + /** * Google Cloud Speech enables easy integration of Google speech recognition * technologies into developer applications. Send audio and receive a text diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php new file mode 100644 index 000000000000..332d10cc7728 --- /dev/null +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php @@ -0,0 +1,1049 @@ +listDatabases($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); + * } + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } + * } + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class DatabaseAdminClient +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'spanner.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; + + private static $instanceNameTemplate; + private static $databaseNameTemplate; + + private $grpcCredentialsHelper; + private $databaseAdminStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + private $operationsClient; + + /** + * Formats a string containing the fully-qualified path to represent + * a instance resource. + */ + public static function formatInstanceName($project, $instance) + { + return self::getInstanceNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a database resource. + */ + public static function formatDatabaseName($project, $instance, $database) + { + return self::getDatabaseNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseProjectFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseInstanceFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['instance']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a database resource. + */ + public static function parseProjectFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a database resource. + */ + public static function parseInstanceFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a database resource. + */ + public static function parseDatabaseFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['database']; + } + + private static function getInstanceNameTemplate() + { + if (self::$instanceNameTemplate == null) { + self::$instanceNameTemplate = new PathTemplate('projects/{project}/instances/{instance}'); + } + + return self::$instanceNameTemplate; + } + + private static function getDatabaseNameTemplate() + { + if (self::$databaseNameTemplate == null) { + self::$databaseNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}'); + } + + return self::$databaseNameTemplate; + } + + private static function getPageStreamingDescriptors() + { + $listDatabasesPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'databases', + ]); + + $pageStreamingDescriptors = [ + 'listDatabases' => $listDatabasesPageStreamingDescriptor, + ]; + + return $pageStreamingDescriptors; + } + + private static function getLongRunningDescriptors() + { + return [ + 'createDatabase' => [ + 'operationReturnType' => '\google\spanner\admin\database\v1\Database', + 'metadataReturnType' => '\google\spanner\admin\database\v1\CreateDatabaseMetadata', + ], + 'updateDatabaseDdl' => [ + 'operationReturnType' => '\google\protobuf\EmptyC', + 'metadataReturnType' => '\google\spanner\admin\database\v1\UpdateDatabaseDdlMetadata', + ], + ]; + } + + /** + * Return an OperationsClient object with the same endpoint as $this. + * + * @return \Google\GAX\LongRunning\OperationsClient + */ + public function getOperationsClient() + { + return $this->operationsClient; + } + + /** + * Resume an existing long running operation that was previously started + * by a long running API method. If $methodName is not provided, or does + * not match a long running API method, then the operation can still be + * resumed, but the OperationResponse object will not deserialize the + * final response. + * + * @param string $operationName The name of the long running operation + * @param string $methodName The name of the method used to start the operation + * + * @return \Google\GAX\OperationResponse + */ + public function resumeOperation($operationName, $methodName = null) + { + $lroDescriptors = self::getLongRunningDescriptors(); + if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) { + $options = $lroDescriptors[$methodName]; + } else { + $options = []; + } + $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options); + $operation->reload(); + + return $operation; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'spanner.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type \Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * \Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner Database Admin API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type \Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ], + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + ]; + $options = array_merge($defaultOptions, $options); + + if (array_key_exists('operationsClient', $options)) { + $this->operationsClient = $options['operationsClient']; + } else { + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + ]); + } + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, + 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'listDatabases' => $defaultDescriptors, + 'createDatabase' => $defaultDescriptors, + 'getDatabase' => $defaultDescriptors, + 'updateDatabaseDdl' => $defaultDescriptors, + 'dropDatabase' => $defaultDescriptors, + 'getDatabaseDdl' => $defaultDescriptors, + 'setIamPolicy' => $defaultDescriptors, + 'getIamPolicy' => $defaultDescriptors, + 'testIamPermissions' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + $longRunningDescriptors = self::getLongRunningDescriptors(); + foreach ($longRunningDescriptors as $method => $longRunningDescriptor) { + $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient]; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/database_admin_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.admin.database.v1.DatabaseAdmin', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (array_key_exists('sslCreds', $options)) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createDatabaseAdminStubFunction = function ($hostname, $opts) { + return new DatabaseAdminGrpcClient($hostname, $opts); + }; + if (array_key_exists('createDatabaseAdminStubFunction', $options)) { + $createDatabaseAdminStubFunction = $options['createDatabaseAdminStubFunction']; + } + $this->databaseAdminStub = $this->grpcCredentialsHelper->createStub( + $createDatabaseAdminStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Lists Cloud Spanner databases. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * // Iterate through all elements + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); + * } + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } + * } + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $parent Required. The instance whose databases should be listed. + * Values are of the form `projects//instances/`. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \Google\GAX\PagedListResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function listDatabases($parent, $optionalArgs = []) + { + $request = new ListDatabasesRequest(); + $request->setParent($parent); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + + $mergedSettings = $this->defaultCallSettings['listDatabases']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'ListDatabases', + $mergedSettings, + $this->descriptors['listDatabases'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Creates a new Cloud Spanner database and starts to prepare it for serving. + * The returned [long-running operation][google.longrunning.Operation] will + * have a name of the format `/operations/` and + * can be used to track preparation of the database. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateDatabaseMetadata][google.spanner.admin.database.v1.CreateDatabaseMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Database][google.spanner.admin.database.v1.Database], if successful. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $createStatement = ""; + * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'createDatabase'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $parent Required. The name of the instance that will serve the new database. + * Values are of the form `projects//instances/`. + * @param string $createStatement Required. A `CREATE DATABASE` statement, which specifies the ID of the + * new database. The database ID must conform to the regular expression + * `[a-z][a-z0-9_\-]*[a-z0-9]` and be between 2 and 30 characters in length. + * @param array $optionalArgs { + * Optional. + * + * @type string[] $extraStatements + * An optional list of DDL statements to run inside the newly created + * database. Statements can create tables, indexes, etc. These + * statements execute atomically with the creation of the database: + * if there is an error in any statement, the database is not created. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\longrunning\Operation + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function createDatabase($parent, $createStatement, $optionalArgs = []) + { + $request = new CreateDatabaseRequest(); + $request->setParent($parent); + $request->setCreateStatement($createStatement); + if (isset($optionalArgs['extraStatements'])) { + foreach ($optionalArgs['extraStatements'] as $elem) { + $request->addExtraStatements($elem); + } + } + + $mergedSettings = $this->defaultCallSettings['createDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'CreateDatabase', + $mergedSettings, + $this->descriptors['createDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets the state of a Cloud Spanner database. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedName = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminClient->getDatabase($formattedName); + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $name Required. The name of the requested database. Values are of the form + * `projects//instances//databases/`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\admin\database\v1\Database + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getDatabase($name, $optionalArgs = []) + { + $request = new GetDatabaseRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'GetDatabase', + $mergedSettings, + $this->descriptors['getDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Updates the schema of a Cloud Spanner database by + * creating/altering/dropping tables, columns, indexes, etc. The returned + * [long-running operation][google.longrunning.Operation] will have a name of + * the format `/operations/` and can be used to + * track execution of the schema change(s). The + * [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateDatabaseDdlMetadata][google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata]. The operation has no response. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $statements = []; + * $operationResponse = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * // operation succeeded and returns no value + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'updateDatabaseDdl'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * // operation succeeded and returns no value + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $database Required. The database to update. + * @param string[] $statements DDL statements to be applied to the database. + * @param array $optionalArgs { + * Optional. + * + * @type string $operationId + * If empty, the new update request is assigned an + * automatically-generated operation ID. Otherwise, `operation_id` + * is used to construct the name of the resulting + * [Operation][google.longrunning.Operation]. + * + * Specifying an explicit operation ID simplifies determining + * whether the statements were executed in the event that the + * [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] call is replayed, + * or the return value is otherwise lost: the [database][google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.database] and + * `operation_id` fields can be combined to form the + * [name][google.longrunning.Operation.name] of the resulting + * [longrunning.Operation][google.longrunning.Operation]: `/operations/`. + * + * `operation_id` should be unique within the database, and must be + * a valid identifier: `[a-zA-Z][a-zA-Z0-9_]*`. Note that + * automatically-generated operation IDs always begin with an + * underscore. If the named operation already exists, + * [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] returns + * `ALREADY_EXISTS`. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\longrunning\Operation + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function updateDatabaseDdl($database, $statements, $optionalArgs = []) + { + $request = new UpdateDatabaseDdlRequest(); + $request->setDatabase($database); + foreach ($statements as $elem) { + $request->addStatements($elem); + } + if (isset($optionalArgs['operationId'])) { + $request->setOperationId($optionalArgs['operationId']); + } + + $mergedSettings = $this->defaultCallSettings['updateDatabaseDdl']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'UpdateDatabaseDdl', + $mergedSettings, + $this->descriptors['updateDatabaseDdl'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Drops (aka deletes) a Cloud Spanner database. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $databaseAdminClient->dropDatabase($formattedDatabase); + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $database Required. The database to be dropped. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function dropDatabase($database, $optionalArgs = []) + { + $request = new DropDatabaseRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['dropDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'DropDatabase', + $mergedSettings, + $this->descriptors['dropDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns the schema of a Cloud Spanner database as a list of formatted + * DDL statements. This method does not show pending schema updates, those may + * be queried using the [Operations][google.longrunning.Operations] API. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminClient->getDatabaseDdl($formattedDatabase); + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $database Required. The database whose schema we wish to get. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\admin\database\v1\GetDatabaseDdlResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getDatabaseDdl($database, $optionalArgs = []) + { + $request = new GetDatabaseDdlRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['getDatabaseDdl']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'GetDatabaseDdl', + $mergedSettings, + $this->descriptors['getDatabaseDdl'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Sets the access control policy on a database resource. Replaces any + * existing policy. + * + * Authorization requires `spanner.databases.setIamPolicy` permission on + * [resource][google.iam.v1.SetIamPolicyRequest.resource]. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $policy = new Policy(); + * $response = $databaseAdminClient->setIamPolicy($formattedResource, $policy); + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being specified. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param Policy $policy REQUIRED: The complete policy to be applied to the `resource`. The size of + * the policy is limited to a few 10s of KB. An empty policy is a + * valid policy but certain Cloud Platform services (such as Projects) + * might reject them. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\iam\v1\Policy + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function setIamPolicy($resource, $policy, $optionalArgs = []) + { + $request = new SetIamPolicyRequest(); + $request->setResource($resource); + $request->setPolicy($policy); + + $mergedSettings = $this->defaultCallSettings['setIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'SetIamPolicy', + $mergedSettings, + $this->descriptors['setIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets the access control policy for a database resource. Returns an empty + * policy if a database exists but does not have a policy set. + * + * Authorization requires `spanner.databases.getIamPolicy` permission on + * [resource][google.iam.v1.GetIamPolicyRequest.resource]. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminClient->getIamPolicy($formattedResource); + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\iam\v1\Policy + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getIamPolicy($resource, $optionalArgs = []) + { + $request = new GetIamPolicyRequest(); + $request->setResource($resource); + + $mergedSettings = $this->defaultCallSettings['getIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'GetIamPolicy', + $mergedSettings, + $this->descriptors['getIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns permissions that the caller has on the specified database resource. + * + * Attempting this RPC on a non-existent Cloud Spanner database will result in + * a NOT_FOUND error if the user has `spanner.databases.list` permission on + * the containing Cloud Spanner instance. Otherwise returns an empty set of + * permissions. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $permissions = []; + * $response = $databaseAdminClient->testIamPermissions($formattedResource, $permissions); + * } finally { + * $databaseAdminClient->close(); + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy detail is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see + * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\iam\v1\TestIamPermissionsResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function testIamPermissions($resource, $permissions, $optionalArgs = []) + { + $request = new TestIamPermissionsRequest(); + $request->setResource($resource); + foreach ($permissions as $elem) { + $request->addPermissions($elem); + } + + $mergedSettings = $this->defaultCallSettings['testIamPermissions']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'TestIamPermissions', + $mergedSettings, + $this->descriptors['testIamPermissions'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->databaseAdminStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json new file mode 100644 index 000000000000..efa919a0a7d8 --- /dev/null +++ b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json @@ -0,0 +1,73 @@ +{ + "interfaces": { + "google.spanner.admin.database.v1.DatabaseAdmin": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 1000, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 32000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "ListDatabases": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CreateDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "UpdateDatabaseDdl": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DropDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetDatabaseDdl": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "SetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "TestIamPermissions": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php new file mode 100644 index 000000000000..bb7d0a10b257 --- /dev/null +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -0,0 +1,1243 @@ +listInstanceConfigs($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); + * } + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } + * } + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class InstanceAdminClient +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'spanner.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; + + private static $projectNameTemplate; + private static $instanceConfigNameTemplate; + private static $instanceNameTemplate; + + private $grpcCredentialsHelper; + private $instanceAdminStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + private $operationsClient; + + /** + * Formats a string containing the fully-qualified path to represent + * a project resource. + */ + public static function formatProjectName($project) + { + return self::getProjectNameTemplate()->render([ + 'project' => $project, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a instance_config resource. + */ + public static function formatInstanceConfigName($project, $instanceConfig) + { + return self::getInstanceConfigNameTemplate()->render([ + 'project' => $project, + 'instance_config' => $instanceConfig, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a instance resource. + */ + public static function formatInstanceName($project, $instance) + { + return self::getInstanceNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a project resource. + */ + public static function parseProjectFromProjectName($projectName) + { + return self::getProjectNameTemplate()->match($projectName)['project']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instance_config resource. + */ + public static function parseProjectFromInstanceConfigName($instanceConfigName) + { + return self::getInstanceConfigNameTemplate()->match($instanceConfigName)['project']; + } + + /** + * Parses the instance_config from the given fully-qualified path which + * represents a instance_config resource. + */ + public static function parseInstanceConfigFromInstanceConfigName($instanceConfigName) + { + return self::getInstanceConfigNameTemplate()->match($instanceConfigName)['instance_config']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseProjectFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseInstanceFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['instance']; + } + + private static function getProjectNameTemplate() + { + if (self::$projectNameTemplate == null) { + self::$projectNameTemplate = new PathTemplate('projects/{project}'); + } + + return self::$projectNameTemplate; + } + + private static function getInstanceConfigNameTemplate() + { + if (self::$instanceConfigNameTemplate == null) { + self::$instanceConfigNameTemplate = new PathTemplate('projects/{project}/instanceConfigs/{instance_config}'); + } + + return self::$instanceConfigNameTemplate; + } + + private static function getInstanceNameTemplate() + { + if (self::$instanceNameTemplate == null) { + self::$instanceNameTemplate = new PathTemplate('projects/{project}/instances/{instance}'); + } + + return self::$instanceNameTemplate; + } + + private static function getPageStreamingDescriptors() + { + $listInstanceConfigsPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'instance_configs', + ]); + $listInstancesPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'instances', + ]); + + $pageStreamingDescriptors = [ + 'listInstanceConfigs' => $listInstanceConfigsPageStreamingDescriptor, + 'listInstances' => $listInstancesPageStreamingDescriptor, + ]; + + return $pageStreamingDescriptors; + } + + private static function getLongRunningDescriptors() + { + return [ + 'createInstance' => [ + 'operationReturnType' => '\google\spanner\admin\instance\v1\Instance', + 'metadataReturnType' => '\google\spanner\admin\instance\v1\CreateInstanceMetadata', + ], + 'updateInstance' => [ + 'operationReturnType' => '\google\spanner\admin\instance\v1\Instance', + 'metadataReturnType' => '\google\spanner\admin\instance\v1\UpdateInstanceMetadata', + ], + ]; + } + + /** + * Return an OperationsClient object with the same endpoint as $this. + * + * @return \Google\GAX\LongRunning\OperationsClient + */ + public function getOperationsClient() + { + return $this->operationsClient; + } + + /** + * Resume an existing long running operation that was previously started + * by a long running API method. If $methodName is not provided, or does + * not match a long running API method, then the operation can still be + * resumed, but the OperationResponse object will not deserialize the + * final response. + * + * @param string $operationName The name of the long running operation + * @param string $methodName The name of the method used to start the operation + * + * @return \Google\GAX\OperationResponse + */ + public function resumeOperation($operationName, $methodName = null) + { + $lroDescriptors = self::getLongRunningDescriptors(); + if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) { + $options = $lroDescriptors[$methodName]; + } else { + $options = []; + } + $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options); + $operation->reload(); + + return $operation; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'spanner.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type \Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * \Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner Instance Admin API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type \Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ], + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + ]; + $options = array_merge($defaultOptions, $options); + + if (array_key_exists('operationsClient', $options)) { + $this->operationsClient = $options['operationsClient']; + } else { + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + ]); + } + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, + 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'listInstanceConfigs' => $defaultDescriptors, + 'getInstanceConfig' => $defaultDescriptors, + 'listInstances' => $defaultDescriptors, + 'getInstance' => $defaultDescriptors, + 'createInstance' => $defaultDescriptors, + 'updateInstance' => $defaultDescriptors, + 'deleteInstance' => $defaultDescriptors, + 'setIamPolicy' => $defaultDescriptors, + 'getIamPolicy' => $defaultDescriptors, + 'testIamPermissions' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + $longRunningDescriptors = self::getLongRunningDescriptors(); + foreach ($longRunningDescriptors as $method => $longRunningDescriptor) { + $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient]; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/instance_admin_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.admin.instance.v1.InstanceAdmin', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (array_key_exists('sslCreds', $options)) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createInstanceAdminStubFunction = function ($hostname, $opts) { + return new InstanceAdminGrpcClient($hostname, $opts); + }; + if (array_key_exists('createInstanceAdminStubFunction', $options)) { + $createInstanceAdminStubFunction = $options['createInstanceAdminStubFunction']; + } + $this->instanceAdminStub = $this->grpcCredentialsHelper->createStub( + $createInstanceAdminStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Lists the supported instance configurations for a given project. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); + * // Iterate through all elements + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); + * } + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } + * } + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $parent Required. The name of the project for which a list of supported instance + * configurations is requested. Values are of the form + * `projects/`. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \Google\GAX\PagedListResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function listInstanceConfigs($parent, $optionalArgs = []) + { + $request = new ListInstanceConfigsRequest(); + $request->setParent($parent); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + + $mergedSettings = $this->defaultCallSettings['listInstanceConfigs']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'ListInstanceConfigs', + $mergedSettings, + $this->descriptors['listInstanceConfigs'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets information about a particular instance configuration. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedName = InstanceAdminClient::formatInstanceConfigName("[PROJECT]", "[INSTANCE_CONFIG]"); + * $response = $instanceAdminClient->getInstanceConfig($formattedName); + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $name Required. The name of the requested instance configuration. Values are of + * the form `projects//instanceConfigs/`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\admin\instance\v1\InstanceConfig + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getInstanceConfig($name, $optionalArgs = []) + { + $request = new GetInstanceConfigRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getInstanceConfig']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetInstanceConfig', + $mergedSettings, + $this->descriptors['getInstanceConfig'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Lists all instances in the given project. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); + * // Iterate through all elements + * $pagedResponse = $instanceAdminClient->listInstances($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); + * } + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstances($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } + * } + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $parent Required. The name of the project for which a list of instances is + * requested. Values are of the form `projects/`. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type string $filter + * An expression for filtering the results of the request. Filter rules are + * case insensitive. The fields eligible for filtering are: + * + * * name + * * display_name + * * labels.key where key is the name of a label + * + * Some examples of using filters are: + * + * * name:* --> The instance has a name. + * * name:Howl --> The instance's name contains the string "howl". + * * name:HOWL --> Equivalent to above. + * * NAME:howl --> Equivalent to above. + * * labels.env:* --> The instance has the label "env". + * * labels.env:dev --> The instance has the label "env" and the value of + * the label contains the string "dev". + * * name:howl labels.env:dev --> The instance's name contains "howl" and + * it has the label "env" with its value + * containing "dev". + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \Google\GAX\PagedListResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function listInstances($parent, $optionalArgs = []) + { + $request = new ListInstancesRequest(); + $request->setParent($parent); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + if (isset($optionalArgs['filter'])) { + $request->setFilter($optionalArgs['filter']); + } + + $mergedSettings = $this->defaultCallSettings['listInstances']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'ListInstances', + $mergedSettings, + $this->descriptors['listInstances'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets information about a particular instance. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $response = $instanceAdminClient->getInstance($formattedName); + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $name Required. The name of the requested instance. Values are of the form + * `projects//instances/`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\admin\instance\v1\Instance + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getInstance($name, $optionalArgs = []) + { + $request = new GetInstanceRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetInstance', + $mergedSettings, + $this->descriptors['getInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Creates an instance and begins preparing it to begin serving. The + * returned [long-running operation][google.longrunning.Operation] + * can be used to track the progress of preparing the new + * instance. The instance name is assigned by the caller. If the + * named instance already exists, `CreateInstance` returns + * `ALREADY_EXISTS`. + * + * Immediately upon completion of this request: + * + * * The instance is readable via the API, with all requested attributes + * but no allocated resources. Its state is `CREATING`. + * + * Until completion of the returned operation: + * + * * Cancelling the operation renders the instance immediately unreadable + * via the API. + * * The instance can be deleted. + * * All other attempts to modify the instance are rejected. + * + * Upon completion of the returned operation: + * + * * Billing for all successfully-allocated resources begins (some types + * may have lower than the requested levels). + * * Databases can be created in the instance. + * * The instance's allocated resource levels are readable via the API. + * * The instance's state becomes `READY`. + * + * The returned [long-running operation][google.longrunning.Operation] will + * have a name of the format `/operations/` and + * can be used to track creation of the instance. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. + * The [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); + * $instanceId = ""; + * $instance = new Instance(); + * $operationResponse = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'createInstance'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $parent Required. The name of the project in which to create the instance. Values + * are of the form `projects/`. + * @param string $instanceId Required. The ID of the instance to create. Valid identifiers are of the + * form `[a-z][-a-z0-9]*[a-z0-9]` and must be between 6 and 30 characters in + * length. + * @param Instance $instance Required. The instance to create. The name may be omitted, but if + * specified must be `/instances/`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\longrunning\Operation + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function createInstance($parent, $instanceId, $instance, $optionalArgs = []) + { + $request = new CreateInstanceRequest(); + $request->setParent($parent); + $request->setInstanceId($instanceId); + $request->setInstance($instance); + + $mergedSettings = $this->defaultCallSettings['createInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'CreateInstance', + $mergedSettings, + $this->descriptors['createInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Updates an instance, and begins allocating or releasing resources + * as requested. The returned [long-running + * operation][google.longrunning.Operation] can be used to track the + * progress of updating the instance. If the named instance does not + * exist, returns `NOT_FOUND`. + * + * Immediately upon completion of this request: + * + * * For resource types for which a decrease in the instance's allocation + * has been requested, billing is based on the newly-requested level. + * + * Until completion of the returned operation: + * + * * Cancelling the operation sets its metadata's + * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins + * restoring resources to their pre-request values. The operation + * is guaranteed to succeed at undoing all resource changes, + * after which point it terminates with a `CANCELLED` status. + * * All other attempts to modify the instance are rejected. + * * Reading the instance via the API continues to give the pre-request + * resource levels. + * + * Upon completion of the returned operation: + * + * * Billing begins for all successfully-allocated resources (some types + * may have lower than the requested levels). + * * All newly-reserved resources are available for serving the instance's + * tables. + * * The instance's new resource levels are readable via the API. + * + * The returned [long-running operation][google.longrunning.Operation] will + * have a name of the format `/operations/` and + * can be used to track the instance modification. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. + * The [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. + * + * Authorization requires `spanner.instances.update` permission on + * resource [name][google.spanner.admin.instance.v1.Instance.name]. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $instance = new Instance(); + * $fieldMask = new FieldMask(); + * $operationResponse = $instanceAdminClient->updateInstance($instance, $fieldMask); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $instanceAdminClient->updateInstance($instance, $fieldMask); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'updateInstance'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param Instance $instance Required. The instance to update, which must always include the instance + * name. Otherwise, only fields mentioned in [][google.spanner.admin.instance.v1.UpdateInstanceRequest.field_mask] need be included. + * @param FieldMask $fieldMask Required. A mask specifying which fields in [][google.spanner.admin.instance.v1.UpdateInstanceRequest.instance] should be updated. + * The field mask must always be specified; this prevents any future fields in + * [][google.spanner.admin.instance.v1.Instance] from being erased accidentally by clients that do not know + * about them. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\longrunning\Operation + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function updateInstance($instance, $fieldMask, $optionalArgs = []) + { + $request = new UpdateInstanceRequest(); + $request->setInstance($instance); + $request->setFieldMask($fieldMask); + + $mergedSettings = $this->defaultCallSettings['updateInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'UpdateInstance', + $mergedSettings, + $this->descriptors['updateInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Deletes an instance. + * + * Immediately upon completion of the request: + * + * * Billing ceases for all of the instance's reserved resources. + * + * Soon afterward: + * + * * The instance and *all of its databases* immediately and + * irrevocably disappear from the API. All data in the databases + * is permanently deleted. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $instanceAdminClient->deleteInstance($formattedName); + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $name Required. The name of the instance to be deleted. Values are of the form + * `projects//instances/` + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function deleteInstance($name, $optionalArgs = []) + { + $request = new DeleteInstanceRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['deleteInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'DeleteInstance', + $mergedSettings, + $this->descriptors['deleteInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Sets the access control policy on an instance resource. Replaces any + * existing policy. + * + * Authorization requires `spanner.instances.setIamPolicy` on + * [resource][google.iam.v1.SetIamPolicyRequest.resource]. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $policy = new Policy(); + * $response = $instanceAdminClient->setIamPolicy($formattedResource, $policy); + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being specified. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param Policy $policy REQUIRED: The complete policy to be applied to the `resource`. The size of + * the policy is limited to a few 10s of KB. An empty policy is a + * valid policy but certain Cloud Platform services (such as Projects) + * might reject them. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\iam\v1\Policy + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function setIamPolicy($resource, $policy, $optionalArgs = []) + { + $request = new SetIamPolicyRequest(); + $request->setResource($resource); + $request->setPolicy($policy); + + $mergedSettings = $this->defaultCallSettings['setIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'SetIamPolicy', + $mergedSettings, + $this->descriptors['setIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets the access control policy for an instance resource. Returns an empty + * policy if an instance exists but does not have a policy set. + * + * Authorization requires `spanner.instances.getIamPolicy` on + * [resource][google.iam.v1.GetIamPolicyRequest.resource]. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $response = $instanceAdminClient->getIamPolicy($formattedResource); + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\iam\v1\Policy + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getIamPolicy($resource, $optionalArgs = []) + { + $request = new GetIamPolicyRequest(); + $request->setResource($resource); + + $mergedSettings = $this->defaultCallSettings['getIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetIamPolicy', + $mergedSettings, + $this->descriptors['getIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns permissions that the caller has on the specified instance resource. + * + * Attempting this RPC on a non-existent Cloud Spanner instance resource will + * result in a NOT_FOUND error if the user has `spanner.instances.list` + * permission on the containing Google Cloud Project. Otherwise returns an + * empty set of permissions. + * + * Sample code: + * ``` + * try { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $permissions = []; + * $response = $instanceAdminClient->testIamPermissions($formattedResource, $permissions); + * } finally { + * $instanceAdminClient->close(); + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy detail is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see + * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\iam\v1\TestIamPermissionsResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function testIamPermissions($resource, $permissions, $optionalArgs = []) + { + $request = new TestIamPermissionsRequest(); + $request->setResource($resource); + foreach ($permissions as $elem) { + $request->addPermissions($elem); + } + + $mergedSettings = $this->defaultCallSettings['testIamPermissions']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'TestIamPermissions', + $mergedSettings, + $this->descriptors['testIamPermissions'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->instanceAdminStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json new file mode 100644 index 000000000000..23dbca4fe655 --- /dev/null +++ b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json @@ -0,0 +1,78 @@ +{ + "interfaces": { + "google.spanner.admin.instance.v1.InstanceAdmin": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 1000, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 32000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "ListInstanceConfigs": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetInstanceConfig": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "ListInstances": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetInstance": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CreateInstance": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "UpdateInstance": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "DeleteInstance": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "SetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "TestIamPermissions": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/Spanner/Bytes.php b/src/Spanner/Bytes.php new file mode 100644 index 000000000000..d3a17aa42094 --- /dev/null +++ b/src/Spanner/Bytes.php @@ -0,0 +1,111 @@ +bytes('hello world'); + * ``` + * + * ``` + * // Byte instances can be cast to base64-encoded strings. + * echo (string) $bytes; + * ``` + */ +class Bytes implements ValueInterface +{ + /** + * @var string|resource|StreamInterface + */ + private $value; + + /** + * @param string|resource|StreamInterface $value The bytes value. + */ + public function __construct($value) + { + $this->value = Psr7\stream_for($value); + } + + /** + * Get the bytes as a stream. + * + * Example: + * ``` + * $stream = $bytes->get(); + * ``` + * + * @return StreamInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * Example: + * ``` + * echo $bytes->type(); + * ``` + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_BYTES; + } + + /** + * Format the value as a string. + * + * Example: + * ``` + * echo $bytes->formatAsString(); + * ``` + * + * @return string + */ + public function formatAsString() + { + return base64_encode((string) $this->value); + } + + /** + * Format the value as a string. + * + * @return string + * @access private + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php new file mode 100644 index 000000000000..d8eea3e9aead --- /dev/null +++ b/src/Spanner/Configuration.php @@ -0,0 +1,194 @@ +spanner(); + * + * $configuration = $spanner->configuration('regional-europe-west'); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig InstanceConfig + * @codingStandardsIgnoreEnd + */ +class Configuration +{ + /** + * @var ConnectionInterface + */ + private $connection; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var array + */ + private $info; + + /** + * Create a configuration instance. + * + * @param ConnectionInterface $connection A service connection for the + * Spanner API. + * @param string $projectId The current project ID. + * @param string $name The simple configuration name. + * @param array $info [optional] A service representation of the + * configuration. + */ + public function __construct( + ConnectionInterface $connection, + $projectId, + $name, + array $info = [] + ) { + $this->connection = $connection; + $this->projectId = $projectId; + $this->name = $name; + $this->info = $info; + } + + /** + * Return the configuration name. + * + * Example: + * ``` + * $name = $configuration->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Return the service representation of the configuration. + * + * This method may require a service call. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * $info = $configuration->info(); + * ``` + * + * @codingStandardsIgnoreStart + * @param array $options [optional] Configuration options. + * @return array [InstanceConfig](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig) + * @codingStandardsIgnoreEnd + */ + public function info(array $options = []) + { + if (!$this->info) { + $this->reload($options); + } + + return $this->info; + } + + /** + * Check if the configuration exists. + * + * This method requires a service call. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * if ($configuration->exists()) { + * echo 'Configuration exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return bool + */ + public function exists(array $options = []) + { + try { + $this->reload($options = []); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Fetch a fresh representation of the configuration from the service. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * $info = $configuration->reload(); + * ``` + * + * @codingStandardsIgnoreStart + * @param array $options [optional] Configuration options. + * @return array [InstanceConfig](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig) + * @codingStandardsIgnoreEnd + */ + public function reload(array $options = []) + { + $this->info = $this->connection->getConfig($options + [ + 'name' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $this->name), + 'projectId' => $this->projectId + ]); + + return $this->info; + } + + /** + * A more readable representation of the object. + * + * @codeCoverageIgnore + * @access private + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'name' => $this->name, + 'info' => $this->info, + ]; + } +} diff --git a/src/Spanner/Connection/ConnectionInterface.php b/src/Spanner/Connection/ConnectionInterface.php new file mode 100644 index 000000000000..70e28b043b5b --- /dev/null +++ b/src/Spanner/Connection/ConnectionInterface.php @@ -0,0 +1,174 @@ + 'setInsert', + 'update' => 'setUpdate', + 'insertOrUpdate' => 'setInsertOrUpdate', + 'replace' => 'setReplace', + 'delete' => 'setDelete' + ]; + + /** + * @var array + */ + private $lroResponseMappers = [ + [ + 'method' => 'updateDatabaseDdl', + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata', + 'message' => protobuf\EmptyC::class + ], [ + 'method' => 'createDatabase', + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata', + 'message' => Database::class + ], [ + 'method' => 'createInstance', + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata', + 'message' => Instance::class + ], [ + 'method' => 'updateInstance', + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', + 'message' => Instance::class + ] + ]; + + /** + * @var array + */ + private $longRunningGrpcClients; + + /** + * @param array $config [optional] + */ + public function __construct(array $config = []) + { + $this->codec = new PhpArray([ + 'customFilters' => [ + 'commitTimestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + }, + 'readTimestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + } + ] + ]); + + $config['codec'] = $this->codec; + $this->setRequestWrapper(new GrpcRequestWrapper($config)); + + $grpcConfig = $this->getGaxConfig(VeneerSpannerClient::VERSION); + $this->instanceAdminClient = new InstanceAdminClient($grpcConfig); + $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); + $this->spannerClient = new SpannerClient($grpcConfig); + $this->operationsClient = $this->instanceAdminClient->getOperationsClient(); + + $this->longRunningGrpcClients = [ + $this->instanceAdminClient, + $this->databaseAdminClient + ]; + } + + /** + * @param array $args [optional] + */ + public function listConfigs(array $args = []) + { + return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ + $this->pluck('projectId', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getConfig(array $args = []) + { + return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function listInstances(array $args = []) + { + return $this->send([$this->instanceAdminClient, 'listInstances'], [ + $this->pluck('projectId', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getInstance(array $args = []) + { + return $this->send([$this->instanceAdminClient, 'getInstance'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function createInstance(array $args = []) + { + $instance = $this->instanceObject($args, true); + $res = $this->send([$this->instanceAdminClient, 'createInstance'], [ + $this->pluck('projectId', $args), + $this->pluck('instanceId', $args), + $instance, + $args + ]); + + return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); + } + + /** + * @param array $args [optional] + */ + public function updateInstance(array $args = []) + { + $instanceObject = $this->instanceObject($args); + + $mask = array_keys($instanceObject->serialize(new PhpArray(['useCamelCase' => false]))); + + $fieldMask = (new protobuf\FieldMask())->deserialize(['paths' => $mask], $this->codec); + + $res = $this->send([$this->instanceAdminClient, 'updateInstance'], [ + $instanceObject, + $fieldMask, + $args + ]); + + return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); + } + + /** + * @param array $args [optional] + */ + public function deleteInstance(array $args = []) + { + return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getInstanceIamPolicy(array $args = []) + { + return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ + $this->pluck('resource', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function setInstanceIamPolicy(array $args = []) + { + return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ + $this->pluck('resource', $args), + $this->pluck('policy', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function testInstanceIamPermissions(array $args = []) + { + return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ + $this->pluck('resource', $args), + $this->pluck('permissions', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function listDatabases(array $args = []) + { + return $this->send([$this->databaseAdminClient, 'listDatabases'], [ + $this->pluck('instance', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function createDatabase(array $args = []) + { + $res = $this->send([$this->databaseAdminClient, 'createDatabase'], [ + $this->pluck('instance', $args), + $this->pluck('createStatement', $args), + $this->pluck('extraStatements', $args), + $args + ]); + + return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); + } + + /** + * @param array $args [optional] + */ + public function updateDatabase(array $args = []) + { + $res = $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [ + $this->pluck('name', $args), + $this->pluck('statements', $args), + $args + ]); + + return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); + } + + /** + * @param array $args [optional] + */ + public function dropDatabase(array $args = []) + { + return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getDatabaseDDL(array $args = []) + { + return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getDatabaseIamPolicy(array $args = []) + { + return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ + $this->pluck('resource', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function setDatabaseIamPolicy(array $args = []) + { + return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ + $this->pluck('resource', $args), + $this->pluck('policy', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function testDatabaseIamPermissions(array $args = []) + { + return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ + $this->pluck('resource', $args), + $this->pluck('permissions', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function createSession(array $args = []) + { + return $this->send([$this->spannerClient, 'createSession'], [ + $this->pluck('database', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getSession(array $args = []) + { + return $this->send([$this->spannerClient, 'getSession'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function deleteSession(array $args = []) + { + return $this->send([$this->spannerClient, 'deleteSession'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function executeSql(array $args = []) + { + $params = $this->pluck('params', $args); + if ($params) { + $args['params'] = (new protobuf\Struct) + ->deserialize($this->formatStructForApi($params), $this->codec); + } + + foreach ($args['paramTypes'] as $key => $param) { + $args['paramTypes'][$key] = (new Type) + ->deserialize($param, $this->codec); + } + + $args['transaction'] = $this->createTransactionSelector($args); + + return $this->send([$this->spannerClient, 'executeSql'], [ + $this->pluck('session', $args), + $this->pluck('sql', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function read(array $args = []) + { + $keySet = $this->pluck('keySet', $args); + $keySet = (new KeySet) + ->deserialize($this->formatKeySet($keySet), $this->codec); + + $args['transaction'] = $this->createTransactionSelector($args); + + return $this->send([$this->spannerClient, 'read'], [ + $this->pluck('session', $args), + $this->pluck('table', $args), + $this->pluck('columns', $args), + $keySet, + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function beginTransaction(array $args = []) + { + $options = new TransactionOptions; + + if (isset($args['transactionOptions']['readOnly'])) { + $ro = $args['transactionOptions']['readOnly']; + + if (isset($ro['minReadTimestamp'])) { + $ro['minReadTimestamp'] = $this->formatTimestampForApi($ro['minReadTimestamp']); + } + + if (isset($ro['readTimestamp'])) { + $ro['readTimestamp'] = $this->formatTimestampForApi($ro['readTimestamp']); + } + + $readOnly = (new TransactionOptions\ReadOnly) + ->deserialize($ro, $this->codec); + + $options->setReadOnly($readOnly); + } else { + $readWrite = new TransactionOptions\ReadWrite(); + $options->setReadWrite($readWrite); + } + + return $this->send([$this->spannerClient, 'beginTransaction'], [ + $this->pluck('session', $args), + $options, + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function commit(array $args = []) + { + $inputMutations = $this->pluck('mutations', $args); + + $mutations = []; + if (is_array($inputMutations)) { + foreach ($inputMutations as $mutation) { + $type = array_keys($mutation)[0]; + $data = $mutation[$type]; + + switch ($type) { + case 'delete': + if (isset($data['keySet'])) { + $data['keySet'] = $this->formatKeySet($data['keySet']); + } + + $operation = (new Mutation\Delete) + ->deserialize($data, $this->codec); + + break; + default: + $data['values'] = $this->formatListForApi($data['values']); + + $operation = (new Mutation\Write) + ->deserialize($data, $this->codec); + + break; + } + + $setterName = $this->mutationSetters[$type]; + $mutation = new Mutation; + $mutation->$setterName($operation); + $mutations[] = $mutation; + } + } + + if (isset($args['singleUseTransaction'])) { + $readWrite = (new TransactionOptions\ReadWrite) + ->deserialize([], $this->codec); + + $options = new TransactionOptions; + $options->setReadWrite($readWrite); + $args['singleUseTransaction'] = $options; + } + + return $this->send([$this->spannerClient, 'commit'], [ + $this->pluck('session', $args), + $mutations, + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function rollback(array $args = []) + { + return $this->send([$this->spannerClient, 'rollback'], [ + $this->pluck('session', $args), + $this->pluck('transactionId', $args), + $args + ]); + } + + /** + * @param array $args + */ + public function getOperation(array $args) + { + $name = $this->pluck('name', $args); + + $operation = $this->getOperationByName($this->databaseAdminClient, $name); + + return $this->operationToArray($operation, $this->codec, $this->lroResponseMappers); + } + + /** + * @param array $args + */ + public function cancelOperation(array $args) + { + $name = $this->pluck('name', $args); + $method = $this->pluck('method', $args); + + $operation = $this->getOperationByNameAndMethod($name, $method); + } + + /** + * @param array $args + */ + public function deleteOperation(array $args) + { + $name = $this->pluck('name', $args); + $method = $this->pluck('method', $args); + + $operation = $this->getOperationByNameAndMethod($name, $method); + } + + /** + * @param array $args + */ + public function listOperations(array $args) + { + $name = $this->pluck('name', $args); + $method = $this->pluck('method', $args); + } + + /** + * @param array $keySet + * @return array Formatted keyset + */ + private function formatKeySet(array $keySet) + { + if (isset($keySet['keys'])) { + $keySet['keys'] = $this->formatListForApi($keySet['keys']); + } + + if (isset($keySet['ranges'])) { + foreach ($keySet['ranges'] as $index => $rangeItem) { + foreach ($rangeItem as $key => $val) { + $rangeItem[$key] = $this->formatListForApi($val); + } + + $keySet['ranges'][$index] = $rangeItem; + } + + if (empty($keySet['ranges'])) { + unset($keySet['ranges']); + } + } + + return $keySet; + } + + /** + * @param array $args + * @return array + */ + private function createTransactionSelector(array &$args) + { + $selector = new TransactionSelector; + if (isset($args['transaction'])) { + $selector = $selector->deserialize($this->pluck('transaction', $args), $this->codec); + } elseif (isset($args['transactionId'])) { + $selector = $selector->deserialize(['id' => $this->pluck('transactionId', $args)], $this->codec); + } + + return $selector; + } + + /** + * @param array $args + * @param bool $isRequired + */ + private function instanceObject(array &$args, $required = false) + { + return (new Instance())->deserialize(array_filter([ + 'name' => $this->pluck('name', $args, $required), + 'config' => $this->pluck('config', $args, $required), + 'displayName' => $this->pluck('displayName', $args, $required), + 'nodeCount' => $this->pluck('nodeCount', $args, $required), + 'state' => $this->pluck('state', $args, $required), + 'labels' => $this->formatLabelsForApi($this->pluck('labels', $args, $required)) + ]), $this->codec); + } +} diff --git a/src/Spanner/Connection/IamDatabase.php b/src/Spanner/Connection/IamDatabase.php new file mode 100644 index 000000000000..f76eb92b11a6 --- /dev/null +++ b/src/Spanner/Connection/IamDatabase.php @@ -0,0 +1,63 @@ +connection = $connection; + } + + /** + * @param array $args + */ + public function getPolicy(array $args) + { + return $this->connection->getDatabaseIamPolicy($args); + } + + /** + * @param array $args + */ + public function setPolicy(array $args) + { + return $this->connection->setDatabaseIamPolicy($args); + } + + /** + * @param array $args + */ + public function testPermissions(array $args) + { + return $this->connection->testDatabaseIamPermissions($args); + } +} diff --git a/src/Spanner/Connection/IamInstance.php b/src/Spanner/Connection/IamInstance.php new file mode 100644 index 000000000000..5dac2b7a7c66 --- /dev/null +++ b/src/Spanner/Connection/IamInstance.php @@ -0,0 +1,63 @@ +connection = $connection; + } + + /** + * @param array $args + */ + public function getPolicy(array $args) + { + return $this->connection->getInstanceIamPolicy($args); + } + + /** + * @param array $args + */ + public function setPolicy(array $args) + { + return $this->connection->setInstanceIamPolicy($args); + } + + /** + * @param array $args + */ + public function testPermissions(array $args) + { + return $this->connection->testInstanceIamPermissions($args); + } +} diff --git a/src/Spanner/Connection/LongRunningConnection.php b/src/Spanner/Connection/LongRunningConnection.php new file mode 100644 index 000000000000..5b75467f25df --- /dev/null +++ b/src/Spanner/Connection/LongRunningConnection.php @@ -0,0 +1,72 @@ +connection = $connection; + } + + /** + * @param array $args + */ + public function get(array $args) + { + return $this->connection->getOperation($args); + } + + /** + * @param array $args + */ + public function cancel(array $args) + { + return $this->connection->cancelOperation($args); + } + + /** + * @param array $args + */ + public function delete(array $args) + { + return $this->connection->deleteOperation($args); + } + + /** + * @param array $args + */ + public function operations(array $args) + { + return $this->connection->listOperations($args); + } +} diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php new file mode 100644 index 000000000000..32ff682b2638 --- /dev/null +++ b/src/Spanner/Database.php @@ -0,0 +1,1122 @@ +spanner(); + * + * $database = $spanner->connect('my-instance', 'my-database'); + * ``` + * + * ``` + * // Databases can also be connected to via an Instance. + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $instance = $spanner->instance('my-instance'); + * $database = $instance->database('my-database'); + * ``` + */ +class Database +{ + use LROTrait; + use TransactionConfigurationTrait; + + const MAX_RETRIES = 3; + + /** + * @var ConnectionInterface + */ + private $connection; + + /** + * @var Instance + */ + private $instance; + + /** + * @var SessionPoolInterface + */ + private $sessionPool; + + /** + * @var LongRunningConnectionInterface + */ + private $lroConnection; + + /** + * @var Operation + */ + private $operation; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var Iam + */ + private $iam; + + /** + * Create an object representing a Database. + * + * @param ConnectionInterface $connection The connection to the + * Google Cloud Spanner Admin API. + * @param Instance $instance The instance in which the database exists. + * @param SessionPoolInterface $sessionPool The session pool implementation. + * @param LongRunningConnectionInterface $lroConnection An implementation + * mapping to methods which handle LRO resolution in the service. + * @param string $projectId The project ID. + * @param string $name The database name. + * @param bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit + * platform compatibility. **Defaults to** false. + */ + public function __construct( + ConnectionInterface $connection, + Instance $instance, + SessionPoolInterface $sessionPool, + LongRunningConnectionInterface $lroConnection, + array $lroCallables, + $projectId, + $name, + $returnInt64AsObject = false + ) { + $this->connection = $connection; + $this->instance = $instance; + $this->sessionPool = $sessionPool; + $this->lroConnection = $lroConnection; + $this->lroCallables = $lroCallables; + $this->projectId = $projectId; + $this->name = $name; + + $this->operation = new Operation($connection, $returnInt64AsObject); + } + + /** + * Return the simple database name. + * + * Example: + * ``` + * $name = $database->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Check if the database exists. + * + * This method sends a service request. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * if ($database->exists()) { + * echo 'Database exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return bool + */ + public function exists(array $options = []) + { + try { + $this->ddl($options); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Update the Database schema by running a SQL statement. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * $database->updateDdl( + * 'CREATE TABLE Users ( + * id INT64 NOT NULL, + * name STRING(100) NOT NULL + * password STRING(100) NOT NULL + * )' + * ); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.UpdateDatabaseDdlRequest UpdateDDLRequest + * @codingStandardsIgnoreEnd + * + * @param string $statement A DDL statements to run against a database. + * @param array $options [optional] Configuration options. + * @return LongRunningOperation + */ + public function updateDdl($statement, array $options = []) + { + return $this->updateDdlBatch([$statement], $options); + } + + /** + * Update the Database schema by running a set of SQL statements. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * $database->updateDdlBatch([ + * 'CREATE TABLE Users ( + * id INT64 NOT NULL, + * name STRING(100) NOT NULL, + * password STRING(100) NOT NULL + * ) PRIMARY KEY (id)', + * 'CREATE TABLE Posts ( + * id INT64 NOT NULL, + * title STRING(100) NOT NULL, + * content STRING(MAX) NOT NULL + * ) PRIMARY KEY(id)' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.UpdateDatabaseDdlRequest UpdateDDLRequest + * @codingStandardsIgnoreEnd + * + * @param string[] $statements A list of DDL statements to run against a database. + * @param array $options [optional] Configuration options. + * @return LongRunningOperation + */ + public function updateDdlBatch(array $statements, array $options = []) + { + $operation = $this->connection->updateDatabase($options + [ + 'name' => $this->fullyQualifiedDatabaseName(), + 'statements' => $statements, + ]); + + return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables); + } + + /** + * Drop the database. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * $database->drop(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DropDatabaseRequest DropDatabaseRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] Configuration options. + * @return void + */ + public function drop(array $options = []) + { + $this->connection->dropDatabase($options + [ + 'name' => $this->fullyQualifiedDatabaseName() + ]); + } + + /** + * Get a list of all database DDL statements. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * $statements = $database->ddl(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#getdatabaseddlrequest GetDatabaseDdlRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function ddl(array $options = []) + { + $ddl = $this->connection->getDatabaseDDL($options + [ + 'name' => $this->fullyQualifiedDatabaseName() + ]); + + if (isset($ddl['statements'])) { + return $ddl['statements']; + } + + return []; + } + + /** + * Manage the database IAM policy + * + * Example: + * ``` + * $iam = $database->iam(); + * ``` + * + * @return Iam + */ + public function iam() + { + if (!$this->iam) { + $this->iam = new Iam( + new IamDatabase($this->connection), + $this->fullyQualifiedDatabaseName() + ); + } + + return $this->iam; + } + + /** + * Create a snapshot to read from a database at a point in time. + * + * If no configuration options are provided, transaction will be opened with + * strong consistency. + * + * Snapshots are executed behind the scenes using a Read-Only Transaction. + * + * Example: + * ``` + * $snapshot = $database->snapshot(); + * ``` + * + * ``` + * // Take a shapshot with a returned timestamp. + * $snapshot = $database->snapshot([ + * 'returnReadTimestamp' => true + * ]); + * + * $timestamp = $snapshot->readTimestamp(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions + * + * @param array $options [optional] { + * Configuration Options + * + * See [ReadOnly](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions.ReadOnly) + * for detailed description of available options. + * + * Please note that only one of `$strong`, `$readTimestamp` or + * `$exactStaleness` may be set in a request. + * + * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected + * read timestamp is included in the Transaction message that + * describes the transaction. + * @type bool $strong Read at a timestamp where all previously committed + * transactions are visible. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type Duration $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. + * } + * @return Snapshot + * @codingStandardsIgnoreEnd + */ + public function snapshot(array $options = []) + { + // These are only available in single-use transactions. + if (isset($options['maxStaleness']) || isset($options['minReadTimestamp'])) { + throw new \BadMethodCallException( + 'maxStaleness and minReadTimestamp are only available in single-use transactions.' + ); + } + + $transactionOptions = $this->configureSnapshotOptions($options); + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + + return $this->operation->snapshot($session, $transactionOptions); + } + + /** + * Execute Read/Write operations inside a Transaction. + * + * Using this method and providing a callable operation provides certain + * benefits including automatic retry when a transaction fails. In case of a + * failure, all transaction operations, including reads, are re-applied in a + * new transaction. + * + * If a transaction exceeds the maximum number of retries, + * {@see Google\Cloud\Core\Exception\AbortedException} will be thrown. Any other + * exception types will immediately bubble up and will interrupt the retry + * operation. + * + * Please note that once a transaction reads data, it will lock the read + * data, preventing other users from modifying that data. For this reason, + * it is important that every transaction commits or rolls back as early as + * possible. Do not hold transactions open longer than necessary. + * + * If you have an active transaction which was obtained from elsewhere, you + * can provide it to this method and gain the benefits of managed retry by + * setting `$options.transaction` to your {@see Google\Cloud\Spanner\Transaction} + * instance. Please note that in this case, it is important that ALL reads + * and mutations MUST be performed within the runTransaction callable. + * + * Example: + * ``` + * $transaction = $database->runTransaction(function (Transaction $t) use ($username, $password) { + * $user = $t->execute('SELECT * FROM Users WHERE Name = @name and PasswordHash = @password', [ + * 'parameters' => [ + * 'name' => $username, + * 'password' => password_hash($password, PASSWORD_DEFAULT) + * ] + * ])->firstRow(); + * + * if ($user) { + * // Do something here to grant the user access. + * // Maybe set a cookie? + * + * $user['loginCount'] = $user['loginCount'] + 1; + * $t->update('Users', $user); + * + * $t->commit(); + * } else { + * $t->rollback(); + * } + * }); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions + * @codingStandardsIgnoreEnd + * + * @param callable $operation The operations to run in the transaction. + * **Signature:** `function (Transaction $transaction)`. + * @param array $options [optional] { + * Configuration Options + * + * @type int $maxRetries The number of times to attempt to apply the + * operation before failing. **Defaults to ** `3`. + * @type Transaction $transaction If provided, the transaction will be + * passed to the callable instead of attempting to begin a new + * transaction. + * } + * @return mixed The return value of `$operation`. + */ + public function runTransaction(callable $operation, array $options = []) + { + $options += [ + 'maxRetries' => self::MAX_RETRIES, + 'transaction' => null + ]; + + // There isn't anything configurable here. + $options['transactionOptions'] = $this->configureTransactionOptions(); + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + $attempt = 0; + $startTransactionFn = function ($session, $options) use (&$attempt) { + if ($attempt === 0 && $options['transaction'] instanceof Transaction) { + $transaction = $options['transaction']; + } elseif ($attempt === 0 && $options['transaction']) { + throw new \InvalidArgumentException('Given transaction must be an instance of Transaction.'); + } else { + $transaction = $this->operation->transaction($session, $options); + } + + $attempt++; + return $transaction; + }; + + $delayFn = function (\Exception $e) { + if (!($e instanceof AbortedException)) { + throw $e; + } + + $delay = $e->getRetryDelay(); + time_nanosleep($delay['seconds'], $delay['nanos']); + }; + + $commitFn = function ($operation, $session, $options) use ($startTransactionFn) { + $transaction = call_user_func_array($startTransactionFn, [ + $session, + $options + ]); + + return call_user_func($operation, $transaction); + }; + + $retry = new Retry($options['maxRetries'], $delayFn); + return $retry->execute($commitFn, [$operation, $session, $options]); + } + + /** + * Create and return a new read/write Transaction. + * + * When manually using a Transaction, it is advised that retry logic be + * implemented to reapply all operations when an instance of + * {@see Google\Cloud\Core\Exception\AbortedException} is thrown. + * + * If you wish Google Cloud PHP to handle retry logic for you (recommended + * for most cases), use {@see Google\Cloud\Spanner\Database::runTransaction()}. + * + * Please note that once a transaction reads data, it will lock the read + * data, preventing other users from modifying that data. For this reason, + * it is important that every transaction commits or rolls back as early as + * possible. Do not hold transactions open longer than necessary. + * + * Example: + * ``` + * $transaction = $database->transaction(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] Configuration Options. + * @return Transaction + */ + public function transaction(array $options = []) + { + // There isn't anything configurable here. + $options['transactionOptions'] = [ + 'readWrite' => [] + ]; + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + return $this->operation->transaction($session, $options); + } + + /** + * Insert a row. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->insert('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Welcome to our site.' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param array $data The row data to insert. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function insert($table, array $data, array $options = []) + { + return $this->insertBatch($table, [$data], $options); + } + + /** + * Insert multiple rows. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->insertBatch('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Welcome to our site.' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'Lots of people ask about where we got started.' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param array $dataSet The row data to insert. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function insertBatch($table, array $dataSet, array $options = []) + { + $mutations = []; + foreach ($dataSet as $data) { + $mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + $options['singleUseTransaction'] = $this->configureTransactionOptions(); + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Update a row. + * + * Only data which you wish to update need be included. You must provide + * enough information for the API to determine which row should be modified. + * In most cases, this means providing values for the Primary Key fields. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->update('Posts', [ + * 'ID' => 1337, + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param array $data The row data to update. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function update($table, array $data, array $options = []) + { + return $this->updateBatch($table, [$data], $options); + } + + /** + * Update multiple rows. + * + * Only data which you wish to update need be included. You must provide + * enough information for the API to determine which row should be modified. + * In most cases, this means providing values for the Primary Key fields. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->updateBatch('Posts', [ + * [ + * 'ID' => 1337, + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param array $dataSet The row data to update. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function updateBatch($table, array $dataSet, array $options = []) + { + $mutations = []; + foreach ($dataSet as $data) { + $mutations[] = $this->operation->mutation(Operation::OP_UPDATE, $table, $data); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + $options['singleUseTransaction'] = $this->configureTransactionOptions(); + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Insert or update a row. + * + * If a row already exists (determined by comparing the Primary Key to + * existing table data), the row will be updated. If not, it will be + * created. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->insertOrUpdate('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param array $data The row data to insert or update. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function insertOrUpdate($table, array $data, array $options = []) + { + return $this->insertOrUpdateBatch($table, [$data], $options); + } + + /** + * Insert or update multiple rows. + * + * If a row already exists (determined by comparing the Primary Key to + * existing table data), the row will be updated. If not, it will be + * created. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->insertOrUpdateBatch('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param array $dataSet The row data to insert or update. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function insertOrUpdateBatch($table, array $dataSet, array $options = []) + { + $mutations = []; + foreach ($dataSet as $data) { + $mutations[] = $this->operation->mutation(Operation::OP_INSERT_OR_UPDATE, $table, $data); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + $options['singleUseTransaction'] = $this->configureTransactionOptions(); + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Replace a row. + * + * Provide data for the entire row. Google Cloud Spanner will attempt to + * find a record matching the Primary Key, and will replace the entire row. + * If a matching row is not found, it will be inserted. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->replace('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param array $data The row data to replace. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function replace($table, array $data, array $options = []) + { + return $this->replaceBatch($table, [$data], $options); + } + + /** + * Replace multiple rows. + * + * Provide data for the entire row. Google Cloud Spanner will attempt to + * find a record matching the Primary Key, and will replace the entire row. + * If a matching row is not found, it will be inserted. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->replaceBatch('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param array $dataSet The row data to replace. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function replaceBatch($table, array $dataSet, array $options = []) + { + $mutations = []; + foreach ($dataSet as $data) { + $mutations[] = $this->operation->mutation(Operation::OP_REPLACE, $table, $data); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + $options['singleUseTransaction'] = $this->configureTransactionOptions(); + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Delete one or more rows. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $keySet = new KeySet([ + * 'keys' => [ + * 1337, 1338 + * ] + * ]); + * + * $database->delete('Posts', $keySet); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * + * @param string $table The table to mutate. + * @param KeySet $keySet The KeySet to identify rows to delete. + * @param array $options [optional] Configuration options. + * @return Timestamp The commit Timestamp. + */ + public function delete($table, KeySet $keySet, array $options = []) + { + $mutations = [$this->operation->deleteMutation($table, $keySet)]; + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + $options['singleUseTransaction'] = $this->configureTransactionOptions(); + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Run a query. + * + * Example: + * ``` + * $result = $database->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 + * ] + * ]); + * ``` + * + * ``` + * // Execute a read and return a new Snapshot for further reads. + * $result = $database->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 + * ], + * 'begin' => true + * ]); + * + * $snapshot = $result->snapshot(); + * ``` + * + * ``` + * // Execute a read and return a new Transaction for further reads and writes. + * $result = $database->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 + * ], + * 'begin' => true, + * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE + * ]); + * + * $transaction = $result->transaction(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ExecuteSqlRequest ExecuteSqlRequest + * @codingStandardsIgnoreEnd + * + * @codingStandardsIgnoreStart + * @param string $sql The query string to execute. + * @param array $options [optional] { + * Configuration Options. + * + * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions) + * for detailed description of available transaction options. + * + * Please note that only one of `$strong`, `$minReadTimestamp`, + * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in + * a request. + * + * @type array $parameters A key/value array of Query Parameters, where + * the key is represented in the query string prefixed by a `@` + * symbol. + * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected + * read timestamp is included in the Transaction message that + * describes the transaction. + * @type bool $strong Read at a timestamp where all previously committed + * transactions are visible. + * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the + * given timestamp. Only available in single-use transactions. + * @type Duration $maxStaleness Read data at a timestamp >= NOW - the + * given timestamp. Only available in single-use transactions. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type Duration $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. + * @type bool $begin If true, will begin a new transaction. If a + * read/write transaction is desired, set the value of + * $transactionType. If a transaction or snapshot is created, it + * will be returned as `$result->transaction()` or + * `$result->snapshot()`. **Defaults to** `false`. + * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ` + * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is + * chosen, any snapshot options will be disregarded. If `$begin` + * is false, this option will be ignored. **Defaults to** + * `SessionPoolInterface::CONTEXT_READ`. + * } + * @codingStandardsIgnoreEnd + * @return Result + */ + public function execute($sql, array $options = []) + { + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; + $options['transactionContext'] = $context; + + return $this->operation->execute($session, $sql, $options); + } + + /** + * Lookup rows in a table. + * + * Example: + * ``` + * $keySet = new KeySet([ + * 'keys' => [1337] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns); + * ``` + * + * ``` + * // Execute a read and return a new Snapshot for further reads. + * $keySet = new KeySet([ + * 'keys' => [1337] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns, [ + * 'begin' => true + * ]); + * + * $snapshot = $result->snapshot(); + * ``` + * + * ``` + * // Execute a read and return a new Transaction for further reads and writes. + * $keySet = new KeySet([ + * 'keys' => [1337] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns, [ + * 'begin' => true, + * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE + * ]); + * + * $transaction = $result->transaction(); + * ``` + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest ReadRequest + * + * @codingStandardsIgnoreStart + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. + * @param array $options [optional] { + * Configuration Options. + * + * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions) + * for detailed description of available transaction options. + * + * Please note that only one of `$strong`, `$minReadTimestamp`, + * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in + * a request. + * + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected + * read timestamp is included in the Transaction message that + * describes the transaction. + * @type bool $strong Read at a timestamp where all previously committed + * transactions are visible. + * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the + * given timestamp. Only available in single-use transactions. + * @type Duration $maxStaleness Read data at a timestamp >= NOW - the + * given timestamp. Only available in single-use transactions. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type Duration $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. + * @type bool $begin If true, will begin a new transaction. If a + * read/write transaction is desired, set the value of + * $transactionType. If a transaction or snapshot is created, it + * will be returned as `$result->transaction()` or + * `$result->snapshot()`. **Defaults to** `false`. + * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ` + * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is + * chosen, any snapshot options will be disregarded. If `$begin` + * is false, this option will be ignored. **Defaults to** + * `SessionPoolInterface::CONTEXT_READ`. + * } + * @codingStandardsIgnoreEnd + * @return Result + */ + public function read($table, KeySet $keySet, array $columns, array $options = []) + { + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; + $options['transactionContext'] = $context; + + return $this->operation->read($session, $table, $keySet, $columns, $options); + } + + /** + * Retrieve a session from the session pool. + * + * @param string $context The session context. + * @return Session + */ + private function selectSession($context = SessionPoolInterface::CONTEXT_READ) + { + return $this->sessionPool->session( + $this->instance->name(), + $this->name, + $context + ); + } + + /** + * Convert the simple database name to a fully qualified name. + * + * @return string + */ + private function fullyQualifiedDatabaseName() + { + return GrpcSpannerClient::formatDatabaseName( + $this->projectId, + $this->instance->name(), + $this->name + ); + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'name' => $this->name, + 'instance' => $this->instance, + 'sessionPool' => $this->sessionPool, + ]; + } +} diff --git a/src/Spanner/Date.php b/src/Spanner/Date.php new file mode 100644 index 000000000000..8822c2bd9f7a --- /dev/null +++ b/src/Spanner/Date.php @@ -0,0 +1,110 @@ +date(new \DateTimeImmutable('1995-02-04')); + * ``` + * + * ``` + * // Date objects can be cast to strings for easy display. + * echo (string) $date; + * ``` + */ +class Date implements ValueInterface +{ + const FORMAT = 'Y-m-d'; + + /** + * @var \DateTimeInterface + */ + protected $value; + + /** + * @param \DateTimeInterface $value The date value. + */ + public function __construct(\DateTimeInterface $value) + { + $this->value = $value; + } + + /** + * Get the underlying `\DateTimeInterface` implementation. + * + * Example: + * ``` + * $dateTime = $date->get(); + * ``` + * + * @return \DateTimeInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * Example: + * ``` + * echo $date->type(); + * ``` + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_DATE; + } + + /** + * Format the value as a string. + * + * Example: + * ``` + * echo $date->formatAsString(); + * ``` + * + * @return string + */ + public function formatAsString() + { + return $this->value->format(self::FORMAT); + } + + /** + * Format the value as a string. + * + * @return string + * @access private + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Duration.php b/src/Spanner/Duration.php new file mode 100644 index 000000000000..c867d55fe99d --- /dev/null +++ b/src/Spanner/Duration.php @@ -0,0 +1,121 @@ +duration($seconds, $nanoSeconds); + * ``` + * + * ``` + * // Duration objects can be cast to json-encoded strings. + * echo (string) $duration; + * ``` + */ +class Duration implements ValueInterface +{ + const TYPE = 'DURATION'; + + /** + * @var int + */ + private $seconds; + + /** + * @var int + */ + private $nanos; + + /** + * @param int $seconds The number of seconds in the duration. + * @param int $nanos The number of nanoseconds in the duration. + */ + public function __construct($seconds, $nanos = 0) + { + $this->seconds = $seconds; + $this->nanos = $nanos; + } + + /** + * Get the duration + * + * Example: + * ``` + * $res = $duration->get(); + * ``` + * + * @return array + */ + public function get() + { + return [ + 'seconds' => $this->seconds, + 'nanos' => $this->nanos + ]; + } + + /** + * Get the type. + * + * Example: + * ``` + * echo $duration->type(); + * ``` + * + * @return string + */ + public function type() + { + return self::TYPE; + } + + /** + * Format the value as a string. + * + * Example: + * ``` + * echo $duration->formatAsString(); + * ``` + * + * @return string + */ + public function formatAsString() + { + return json_encode($this->get()); + } + + /** + * Format the value as a string. + * + * @return string + * @access private + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php new file mode 100644 index 000000000000..44e029983010 --- /dev/null +++ b/src/Spanner/Instance.php @@ -0,0 +1,472 @@ +spanner(); + * + * $instance = $spanner->instance('my-instance'); + * ``` + */ +class Instance +{ + use ArrayTrait; + use LROTrait; + + const STATE_READY = State::READY; + const STATE_CREATING = State::CREATING; + + /** + * @var ConnectionInterface + */ + private $connection; + + /** + * @var SessionPool; + */ + private $sessionPool; + + /** + * @var LongRunningConnectionInterface + */ + private $lroConnection; + + /** + * @var array + */ + private $lroCallables; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var bool + */ + private $returnInt64AsObject; + + /** + * @var array + */ + private $info; + + /** + * @var Iam + */ + private $iam; + + /** + * Create an object representing a Google Cloud Spanner instance. + * + * @param ConnectionInterface $connection The connection to the + * Google Cloud Spanner Admin API. + * @param SessionPoolInterface $sessionPool The session pool implementation. + * @param LongRunningConnectionInterface $lroConnection An implementation + * mapping to methods which handle LRO resolution in the service. + * @param array $lroCallables + * @param string $projectId The project ID. + * @param string $name The instance name. + * @param bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit platform + * compatibility. **Defaults to** false. + * @param array $info [optional] A representation of the instance object. + */ + public function __construct( + ConnectionInterface $connection, + SessionPoolInterface $sessionPool, + LongRunningConnectionInterface $lroConnection, + array $lroCallables, + $projectId, + $name, + $returnInt64AsObject = false, + array $info = [] + ) { + $this->connection = $connection; + $this->sessionPool = $sessionPool; + $this->lroConnection = $lroConnection; + $this->lroCallables = $lroCallables; + $this->projectId = $projectId; + $this->name = $name; + $this->returnInt64AsObject = $returnInt64AsObject; + $this->info = $info; + } + + /** + * Return the instance name. + * + * Example: + * ``` + * $name = $instance->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Return the service representation of the instance. + * + * This method may require a service call. + * + * Example: + * ``` + * $info = $instance->info(); + * echo $info['nodeCount']; + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function info(array $options = []) + { + if (!$this->info) { + $this->reload($options); + } + + return $this->info; + } + + /** + * Check if the instance exists. + * + * This method requires a service call. + * + * Example: + * ``` + * if ($instance->exists()) { + * echo 'Instance exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function exists(array $options = []) + { + try { + $this->reload($options = []); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Fetch a fresh representation of the instance from the service. + * + * Example: + * ``` + * $info = $instance->reload(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.GetInstanceRequest GetInstanceRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function reload(array $options = []) + { + $this->info = $this->connection->getInstance($options + [ + 'name' => $this->fullyQualifiedInstanceName() + ]); + + return $this->info; + } + + /** + * Return the instance state. + * + * When instances are created or updated, they may take some time before + * they are ready for use. This method allows for checking whether an + * instance is ready. + * + * Example: + * ``` + * if ($instance->state() === Instance::STATE_READY) { + * echo 'Instance is ready!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return string + */ + public function state(array $options = []) + { + $info = $this->info($options); + + return (isset($info['state'])) + ? $info['state'] + : null; + } + + /** + * Update the instance + * + * Example: + * ``` + * $instance->update([ + * 'displayName' => 'My Instance', + * 'nodeCount' => 4 + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#updateinstancerequest UpdateInstanceRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] { + * Configuration options + * + * @type string $displayName The descriptive name for this instance as + * it appears in UIs. **Defaults to** the value of $name. + * @type int $nodeCount The number of nodes allocated to this instance. + * **Defaults to** `1`. + * @type array $labels For more information, see + * [Using labels to organize Google Cloud Platform resources](https://goo.gl/xmQnxf). + * } + * @return LongRunningOperation + * @throws \InvalidArgumentException + */ + public function update(array $options = []) + { + $info = $this->info($options); + + $options += [ + 'displayName' => $info['displayName'], + 'nodeCount' => (isset($info['nodeCount'])) ? $info['nodeCount'] : null, + 'labels' => (isset($info['labels'])) + ? $info['labels'] + : [] + ]; + + $operation = $this->connection->updateInstance([ + 'name' => $this->fullyQualifiedInstanceName(), + ] + $options); + + return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables); + } + + /** + * Delete the instance, any databases in the instance, and all data. + * + * Example: + * ``` + * $instance->delete(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#deleteinstancerequest DeleteInstanceRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] Configuration options. + * @return void + */ + public function delete(array $options = []) + { + return $this->connection->deleteInstance($options + [ + 'name' => $this->fullyQualifiedInstanceName() + ]); + } + + /** + * Create a Database + * + * Example: + * ``` + * $database = $instance->createDatabase('my-database'); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#createdatabaserequest CreateDatabaseRequest + * @codingStandardsIgnoreEnd + * + * @param string $name The database name. + * @param array $options [optional] { + * Configuration Options + * + * @type array $statements Additional DDL statements. + * } + * @return LongRunningOperation + */ + public function createDatabase($name, array $options = []) + { + $options += [ + 'statements' => [], + ]; + + $statement = sprintf('CREATE DATABASE `%s`', $name); + + $operation = $this->connection->createDatabase([ + 'instance' => $this->fullyQualifiedInstanceName(), + 'createStatement' => $statement, + 'extraStatements' => $options['statements'] + ]); + + return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables); + } + + /** + * Lazily instantiate a database object + * + * Example: + * ``` + * $database = $instance->database('my-database'); + * ``` + * + * @param string $name The database name + * @return Database + */ + public function database($name) + { + return new Database( + $this->connection, + $this, + $this->sessionPool, + $this->lroConnection, + $this->lroCallables, + $this->projectId, + $name, + $this->returnInt64AsObject + ); + } + + /** + * List databases in an instance + * + * Example: + * ``` + * $databases = $instance->databases(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.database.v1#listdatabasesrequest ListDatabasesRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] { + * Configuration options. + * + * @type int $pageSize Maximum number of results to return per + * request. + * @type int $resultLimit Limit the number of results returned in total. + * **Defaults to** `0` (return all results). + * @type string $pageToken A previously-returned page token used to + * resume the loading of results from a specific point. + * } + * @return ItemIterator + */ + public function databases(array $options = []) + { + $resultLimit = $this->pluck('resultLimit', $options, false); + return new ItemIterator( + new PageIterator( + function (array $database) { + $name = DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']); + return $this->database($name); + }, + [$this->connection, 'listDatabases'], + $options + ['instance' => $this->fullyQualifiedInstanceName()], + [ + 'itemsKey' => 'databases', + 'resultLimit' => $resultLimit + ] + ) + ); + } + + /** + * Manage the instance IAM policy + * + * Example: + * ``` + * $iam = $instance->iam(); + * ``` + * + * @return Iam + */ + public function iam() + { + if (!$this->iam) { + $this->iam = new Iam( + new IamInstance($this->connection), + $this->fullyQualifiedInstanceName() + ); + } + + return $this->iam; + } + + /** + * Convert the simple instance name to a fully qualified name. + * + * @return string + */ + private function fullyQualifiedInstanceName() + { + return InstanceAdminClient::formatInstanceName($this->projectId, $this->name); + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'name' => $this->name, + 'info' => $this->info + ]; + } +} diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php new file mode 100644 index 000000000000..ecd45a1f9529 --- /dev/null +++ b/src/Spanner/KeyRange.php @@ -0,0 +1,237 @@ +spanner(); + * + * // Create a KeyRange for all people named Bob, born in 1969. + * $start = $spanner->date(new \DateTime('1969-01-01')); + * $end = $spanner->date(new \DateTime('1969-12-31')); + * + * $range = $spanner->keyRange([ + * 'startType' => KeyRange::TYPE_CLOSED, + * 'start' => ['Bob', $start], + * 'endType' => KeyRange::TYPE_CLOSED, + * 'end' => ['Bob', $end] + * ]); + * ``` + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.KeyRange KeyRange + */ +class KeyRange +{ + const TYPE_OPEN = 'open'; + const TYPE_CLOSED = 'closed'; + + /** + * @var string + */ + private $startType; + + /** + * @var array + */ + private $start; + + /** + * @var string + */ + private $endType; + + /** + * @var array + */ + private $end; + + /** + * @var array + */ + private $definition = [ + self::TYPE_OPEN => [ + 'start' => 'startOpen', + 'end' => 'endOpen' + ], + self::TYPE_CLOSED => [ + 'start' => 'startClosed', + 'end' => 'endClosed' + ] + ]; + + /** + * Create a KeyRange. + * + * @param array $options [optional] { + * Configuration Options. + * + * @type string $startType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $start The key with which to start the range. + * @type string $endType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $end The key with which to end the range. + * } + */ + public function __construct(array $options = []) + { + $options += [ + 'startType' => null, + 'start' => null, + 'endType' => null, + 'end' => null + ]; + + $this->startType = $options['startType']; + $this->start = $options['start']; + $this->endType = $options['endType']; + $this->end = $options['end']; + } + + /** + * Get the range start. + * + * Example: + * ``` + * $start = $range->start(); + * ``` + * + * @return array|null + */ + public function start() + { + return $this->start; + } + + /** + * Set the range start. + * + * Example: + * ``` + * $range->setStart(KeyRange::TYPE_OPEN, ['Bob']); + * ``` + * + * @param string $type Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed + * correctness. + * @param array $start The start of the key range. + * @return void + */ + public function setStart($type, array $start) + { + if (!in_array($type, array_keys($this->definition))) { + throw new \InvalidArgumentException(sprintf( + 'Invalid KeyRange type. Allowed values are %s', + implode(', ', array_keys($this->definition)) + )); + } + + $rangeKey = $this->definition[$type]['start']; + + $this->startType = $rangeKey; + $this->start = $start; + } + + /** + * Get the range end. + * + * Example: + * ``` + * $end = $range->end(); + * ``` + * + * @return array + */ + public function end() + { + return $this->end; + } + + /** + * Set the range end. + * + * Example: + * ``` + * $range->setEnd(KeyRange::TYPE_CLOSED, ['Jill']); + * ``` + * + * @param string $type Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed + * correctness. + * @param array $end The end of the key range. + * @return void + */ + public function setEnd($type, array $end) + { + if (!in_array($type, array_keys($this->definition))) { + throw new \InvalidArgumentException(sprintf( + 'Invalid KeyRange type. Allowed values are %s', + implode(', ', array_keys($this->definition)) + )); + } + + $rangeKey = $this->definition[$type]['end']; + + $this->endType = $rangeKey; + $this->end = $end; + } + + /** + * Get the start and end types + * + * Example: + * ``` + * $types = $range->types(); + * ``` + * + * @return array An array containing `start` and `end` keys. + */ + public function types() + { + return [ + 'start' => $this->startType, + 'end' => $this->endType + ]; + } + + /** + * Returns an API-compliant representation of a KeyRange. + * + * @return array + * @access private + */ + public function keyRangeObject() + { + if (!$this->start || !$this->end) { + throw new \BadMethodCallException('Key Range must supply a start and an end'); + } + + return [ + $this->startType => $this->start, + $this->endType => $this->end + ]; + } +} diff --git a/src/Spanner/KeySet.php b/src/Spanner/KeySet.php new file mode 100644 index 000000000000..1ee4259f9bac --- /dev/null +++ b/src/Spanner/KeySet.php @@ -0,0 +1,239 @@ +keySet(); + * ``` + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#keyset KeySet + */ +class KeySet +{ + use ValidateTrait; + + /** + * @var array + */ + private $keys; + + /** + * @var KeyRange[] + */ + private $ranges; + + /** + * @var bool + */ + private $all; + + /** + * Create a KeySet. + * + * @param array $options [optional] { + * @type array $keys A list of specific keys. Entries in keys should + * have exactly as many elements as there are columns in the + * primary or index key with which this KeySet is used. + * @type KeyRange[] $ranges A list of Key Ranges. + * @type bool $all If true, KeySet will match all keys in a table. + * **Defaults to** `false`. + * } + */ + public function __construct(array $options = []) + { + $options += [ + 'keys' => [], + 'ranges' => [], + 'all' => false + ]; + + $this->validateBatch($options['ranges'], KeyRange::class); + + $this->keys = $options['keys']; + $this->ranges = $options['ranges']; + $this->all = (bool) $options['all']; + } + + /** + * Fetch the KeyRanges + * + * Example: + * ``` + * $ranges = $keySet->ranges(); + * ``` + * + * @return KeyRange[] + */ + public function ranges() + { + return $this->ranges; + } + + + /** + * Add a single KeyRange. + * + * Example: + * ``` + * $range = new KeyRange(); + * $keySet->addRange($range); + * ``` + * + * @param KeyRange $range A KeyRange instance. + * @return void + */ + public function addRange(KeyRange $range) + { + $this->ranges[] = $range; + } + + /** + * Set the KeySet's KeyRanges. + * + * Any existing KeyRanges will be overridden. + * + * Example: + * ``` + * $range = new KeyRange(); + * $keySet->setRanges([$range]); + * ``` + * + * @param KeyRange[] $ranges An array of KeyRange objects. + * @return void + */ + public function setRanges(array $ranges) + { + $this->validateBatch($ranges, KeyRange::class); + + $this->ranges = $ranges; + } + + /** + * Fetch the keys. + * + * Example: + * ``` + * $keys = $keySet->keys(); + * ``` + * + * @return mixed[] + */ + public function keys() + { + return $this->keys; + } + + /** + * Add a single key. + * + * A Key should have exactly as many elements as there are columns in the + * primary or index key with which this KeySet is used. + * + * Example: + * ``` + * $keySet->addKey('Bob'); + * ``` + * + * @param mixed $key The Key to add. + * @return void + */ + public function addKey($key) + { + $this->keys[] = $key; + } + + /** + * Set the KeySet keys. + * + * Any existing keys will be overridden. + * + * Example: + * ``` + * $keySet->setKeys(['Bob', 'Jill']); + * ``` + * + * @param mixed[] $keys + * @return void + */ + public function setKeys(array $keys) + { + $this->keys = $keys; + } + + /** + * Get the value of Match All. + * + * Example: + * ``` + * if ($keySet->matchAll()) { + * echo "All keys will match"; + * } + * ``` + * + * @return bool + */ + public function matchAll() + { + return $this->all; + } + + /** + * Choose whether the KeySet should match all keys in a table. + * + * Example: + * ``` + * $keySet->setMatchAll(true); + * ``` + * + * @param bool $all If true, all keys in a table will be matched. + * @return void + */ + public function setMatchAll($all) + { + $this->all = (bool) $all; + } + + /** + * Format a KeySet object for use in the Spanner API. + * + * @access private + */ + public function keySetObject() + { + $ranges = []; + foreach ($this->ranges as $range) { + $ranges[] = $range->keyRangeObject(); + } + + return [ + 'keys' => $this->keys, + 'ranges' => $ranges, + 'all' => $this->all + ]; + } +} diff --git a/src/Spanner/LICENSE b/src/Spanner/LICENSE new file mode 100644 index 000000000000..8f71f43fee3f --- /dev/null +++ b/src/Spanner/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php new file mode 100644 index 000000000000..b242972b5ea1 --- /dev/null +++ b/src/Spanner/Operation.php @@ -0,0 +1,382 @@ +connection = $connection; + $this->mapper = new ValueMapper($returnInt64AsObject); + } + + /** + * Create a formatted mutation. + * + * @param string $operation The operation type. + * @param string $table The table name. + * @param array $mutation The mutation data, represented as a set of + * key/value pairs. + * @return array + */ + public function mutation($operation, $table, $mutation) + { + $mutation = $this->arrayFilterRemoveNull($mutation); + + return [ + $operation => [ + 'table' => $table, + 'columns' => array_keys($mutation), + 'values' => $this->mapper->encodeValuesAsSimpleType(array_values($mutation)) + ] + ]; + } + + /** + * Create a formatted delete mutation. + * + * @param string $table The table name. + * @param KeySet $keySet The keys to delete. + * @return array + */ + public function deleteMutation($table, KeySet $keySet) + { + return [ + self::OP_DELETE => [ + 'table' => $table, + 'keySet' => $this->flattenKeySet($keySet), + ] + ]; + } + + /** + * Commit all enqueued mutations. + * + * @codingStandardsIgnoreStart + * @param Session $session The session ID to use for the commit. + * @param Transaction $transaction The transaction to commit. + * @param array $options [optional] { + * Configuration options. + * + * @type string $transactionId The ID of the transaction. + * } + * @return Timestamp The commit Timestamp. + */ + public function commit(Session $session, array $mutations, array $options = []) + { + $options += [ + 'transactionId' => null + ]; + + $res = $this->connection->commit($this->arrayFilterRemoveNull([ + 'mutations' => $mutations, + 'session' => $session->name() + ]) + $options); + + return $this->mapper->createTimestampWithNanos($res['commitTimestamp']); + } + + /** + * Rollback a Transaction + * + * @param Session $session The session to use for the rollback. + * Note that the session MUST be the same one in which the + * transaction was created. + * @param string $transactionId The transaction to roll back. + * @param array $options [optional] Configuration Options. + * @return void + */ + public function rollback(Session $session, $transactionId, array $options = []) + { + return $this->connection->rollback([ + 'transactionId' => $transactionId, + 'session' => $session->name() + ] + $options); + } + + /** + * Run a query + * + * @param Session $session The session to use to execute the SQL. + * @param string $sql The query string. + * @param array $options [optional] Configuration options. + * @return array + */ + public function execute(Session $session, $sql, array $options = []) + { + $options += [ + 'parameters' => [], + 'transactionContext' => null + ]; + + $parameters = $this->pluck('parameters', $options); + $options += $this->mapper->formatParamsForExecuteSql($parameters); + + $context = $this->pluck('transactionContext', $options); + + $res = $this->connection->executeSql([ + 'sql' => $sql, + 'session' => $session->name() + ] + $options); + + return $this->createResult($session, $res, $context); + } + + /** + * Lookup rows in a database. + * + * @param Session $session The session in which to read data. + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * } + * @return Result + */ + public function read(Session $session, $table, KeySet $keySet, array $columns, array $options = []) + { + $options += [ + 'index' => null, + 'limit' => null, + 'offset' => null, + 'transactionContext' => null + ]; + + $context = $this->pluck('transactionContext', $options); + $res = $this->connection->read([ + 'table' => $table, + 'session' => $session->name(), + 'columns' => $columns, + 'keySet' => $this->flattenKeySet($keySet) + ] + $options); + + return $this->createResult($session, $res, $context); + } + + /** + * Create a read/write transaction. + * + * @todo if a transaction is already available on the session, get it instead + * of starting a new one? + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the transaction in. + * @param array $options [optional] Configuration options. + * @return Transaction + */ + public function transaction(Session $session, array $options = []) + { + $res = $this->beginTransaction($session, $options); + return $this->createTransaction($session, $res); + } + + /** + * Create a read-only snapshot transaction. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the snapshot in. + * @param array $options [optional] Configuration options. + * @return Snapshot + */ + public function snapshot(Session $session, array $options = []) + { + $res = $this->beginTransaction($session, $options); + + return $this->createSnapshot($session, $res); + } + + /** + * Execute a service call to begin a transaction or snapshot. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the snapshot in. + * @param array $options [optional] Configuration options. + * @return array + */ + private function beginTransaction(Session $session, array $options = []) + { + $options += [ + 'transactionOptions' => [] + ]; + + return $this->connection->beginTransaction($options + [ + 'session' => $session->name(), + ]); + } + + /** + * Create a Transaction instance from a response object. + * + * @param Session $session The session the transaction belongs to. + * @param array $res The transaction response. + * @return Transaction + */ + private function createTransaction(Session $session, array $res) + { + return new Transaction($this, $session, $res['id']); + } + + /** + * Create a Snapshot instance from a response object. + * + * @param Session $session The session the snapshot belongs to. + * @param array $res The snapshot response. + * @return Snapshot + */ + private function createSnapshot(Session $session, array $res) + { + $timestamp = null; + if (isset($res['readTimestamp'])) { + $timestamp = $this->mapper->createTimestampWithNanos($res['readTimestamp']); + } + + return new Snapshot($this, $session, $res['id'], $timestamp); + } + + /** + * Transform a service read or executeSql response to a friendly result. + * + * @codingStandardsIgnoreStart + * @param Session $session The current session. + * @param array $res [ResultSet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet) + * @param string $transactionContext + * @return Result + * @codingStandardsIgnoreEnd + */ + private function createResult(Session $session, array $res, $transactionContext) + { + $columns = isset($res['metadata']['rowType']['fields']) + ? $res['metadata']['rowType']['fields'] + : []; + + $rows = []; + if (isset($res['rows'])) { + foreach ($res['rows'] as $row) { + $rows[] = $this->mapper->decodeValues($columns, $row); + } + } + + $options = []; + if (isset($res['metadata']['transaction']['id'])) { + if ($transactionContext === SessionPoolInterface::CONTEXT_READ) { + $options['snapshot'] = $this->createSnapshot($session, $res['metadata']['transaction']); + } else { + $options['transaction'] = $this->createTransaction($session, $res['metadata']['transaction']); + } + } + + return new Result($res, $rows, $options); + } + + /** + * Convert a KeySet object to an API-ready array. + * + * @param KeySet $keySet The keySet object. + * @return array [KeySet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#keyset) + */ + private function flattenKeySet(KeySet $keySet) + { + $keyRanges = $keySet->ranges(); + if ($keyRanges) { + $ranges = []; + foreach ($keyRanges as $range) { + $types = $range->types(); + + $start = $range->start(); + $range->setStart($types['start'], $this->mapper->encodeValuesAsSimpleType($start)); + + $end = $range->end(); + $range->setEnd($types['end'], $this->mapper->encodeValuesAsSimpleType($end)); + + $ranges[] = $range; + } + + $keySet->setRanges($ranges); + } + + $keys = $keySet->keySetObject(); + if (!empty($keys['keys'])) { + $keys['keys'] = $this->mapper->encodeValuesAsSimpleType($keys['keys']); + } + + return $this->arrayFilterRemoveNull($keys); + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + ]; + } +} diff --git a/src/Spanner/README.md b/src/Spanner/README.md new file mode 100644 index 000000000000..de583242a7c3 --- /dev/null +++ b/src/Spanner/README.md @@ -0,0 +1,16 @@ +# Google Cloud PHP Spanner + +> Idiomatic PHP client for [Cloud Spanner](https://cloud.google.com/spanner/). + +* [Homepage](http://googlecloudplatform.github.io/google-cloud-php) +* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-spanner/latest/spanner/spannerclient) + +**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any +support requests, bug reports, or development contributions should be directed to +that project. + +## Installation + +``` +$ composer require google/cloud-spanner +``` diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php new file mode 100644 index 000000000000..c168438ce963 --- /dev/null +++ b/src/Spanner/Result.php @@ -0,0 +1,203 @@ +spanner(); + * $database = $spanner->connect('my-instance', 'my-database'); + * + * $result = $database->execute('SELECT * FROM Posts'); + * ``` + * + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet ResultSet + */ +class Result implements \IteratorAggregate +{ + /** + * @var array + */ + private $result; + + /** + * @var array + */ + private $rows; + + /** + * @var array + */ + private $options; + + /** + * @param array $result The query or read result. + * @param array $rows The rows, formatted and decoded. + * @param array $options Additional result options and info. + */ + public function __construct(array $result, array $rows, array $options = []) + { + $this->result = $result; + $this->rows = $rows; + $this->options = $options; + } + + /** + * Return result metadata + * + * Example: + * ``` + * $metadata = $result->metadata(); + * ``` + * + * @codingStandardsIgnoreStart + * @return array [ResultSetMetadata](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetMetadata). + * @codingStandardsIgnoreEnd + */ + public function metadata() + { + return $this->result['metadata']; + } + + /** + * Return the formatted and decoded rows. + * + * Example: + * ``` + * $rows = $result->rows(); + * ``` + * + * @return array|null + */ + public function rows() + { + return $this->rows; + } + + /** + * Return the first row, or null. + * + * Useful when selecting a single row. + * + * Example: + * ``` + * $row = $result->firstRow(); + * ``` + * + * @return array|null + */ + public function firstRow() + { + return (isset($this->rows[0])) + ? $this->rows[0] + : null; + } + + /** + * Get the query plan and execution statistics for the query that produced + * this result set. + * + * Stats are not returned by default. + * + * Example: + * ``` + * $stats = $result->stats(); + * ``` + * + * ``` + * // Executing a query with stats returned. + * $res = $database->execute('SELECT * FROM Posts', [ + * 'queryMode' => 'PROFILE' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @return array|null [ResultSetStats](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetStats). + * @codingStandardsIgnoreEnd + */ + public function stats() + { + return (isset($this->result['stats'])) + ? $this->result['stats'] + : null; + } + + /** + * Returns a transaction which was begun in the read or execute, if one exists. + * + * Example: + * ``` + * $transaction = $result->transaction(); + * ``` + * + * @return Transaction|null + */ + public function transaction() + { + return (isset($this->options['transaction'])) + ? $this->options['transaction'] + : null; + } + + /** + * Returns a snapshot which was begun in the read or execute, if one exists. + * + * Example: + * ``` + * $snapshot = $result->snapshot(); + * ``` + * + * @return Snapshot|null + */ + public function snapshot() + { + return (isset($this->options['snapshot'])) + ? $this->options['snapshot'] + : null; + } + + /** + * Get the entire query or read response as given by the API. + * + * Example: + * ``` + * $info = $result->info(); + * ``` + * + * @codingStandardsIgnoreStart + * @return array [ResultSet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet). + * @codingStandardsIgnoreEnd + */ + public function info() + { + return $this->result; + } + + /** + * @access private + */ + public function getIterator() + { + return new \ArrayIterator($this->rows); + } +} diff --git a/src/Spanner/Session/Session.php b/src/Spanner/Session/Session.php new file mode 100644 index 000000000000..0ecf8f3d3a00 --- /dev/null +++ b/src/Spanner/Session/Session.php @@ -0,0 +1,145 @@ +connection = $connection; + $this->projectId = $projectId; + $this->instance = $instance; + $this->database = $database; + $this->name = $name; + } + + /** + * Return info on the session + * + * @return array An array containing the `projectId`, `instance`, `database` and session `name` keys. + */ + public function info() + { + return [ + 'projectId' => $this->projectId, + 'instance' => $this->instance, + 'database' => $this->database, + 'name' => $this->name + ]; + } + + /** + * Check if the session exists. + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function exists(array $options = []) + { + try { + $this->connection->getSession($options + [ + 'name' => $this->name() + ]); + + return true; + } catch (NotFoundException $e) { + return false; + } + } + + /** + * Delete the session. + * + * @param array $options [optional] Configuration options. + * @return void + */ + public function delete(array $options = []) + { + return $this->connection->deleteSession($options + [ + 'name' => $this->name() + ]); + } + + /** + * Format the constituent parts of a session name into a fully qualified session name. + * + * @return string + */ + public function name() + { + return SpannerClient::formatSessionName( + $this->projectId, + $this->instance, + $this->database, + $this->name + ); + } + + /** + * @access private + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'instance' => $this->instance, + 'database' => $this->database, + 'name' => $this->name, + ]; + } +} diff --git a/src/Spanner/Session/SessionClient.php b/src/Spanner/Session/SessionClient.php new file mode 100644 index 000000000000..1642e9519c3a --- /dev/null +++ b/src/Spanner/Session/SessionClient.php @@ -0,0 +1,104 @@ +connection = $connection; + $this->projectId = $projectId; + } + + /** + * Create a new session in the given instance and database. + * + * @param string $instance The simple instance name. + * @param string $database The simple database name. + * @param array $options [optional] Configuration options. + * @return Session|null If the operation succeeded, a Session object will be returned, + * otherwise null. + */ + public function create($instance, $database, array $options = []) + { + $res = $this->connection->createSession($options + [ + 'database' => SpannerClient::formatDatabaseName($this->projectId, $instance, $database) + ]); + + $session = null; + if (isset($res['name'])) { + $session = $this->session($res['name']); + } + + return $session; + } + + /** + * Get a Session + * + * @param string $sessionName The Session name. + * @return Session + */ + public function session($sessionName) + { + return new Session( + $this->connection, + $this->projectId, + SpannerClient::parseInstanceFromSessionName($sessionName), + SpannerClient::parseDatabaseFromSessionName($sessionName), + SpannerClient::parseSessionFromSessionName($sessionName) + ); + } + + /** + * @access private + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId + ]; + } +} diff --git a/src/Spanner/Session/SessionPool.php b/src/Spanner/Session/SessionPool.php new file mode 100644 index 000000000000..2c96d8b12e88 --- /dev/null +++ b/src/Spanner/Session/SessionPool.php @@ -0,0 +1,63 @@ +sessionClient = $sessionClient; + } + + /** + * @access private + */ + public function addSession(Session $session) + { + $this->sessions[] = $sessions; + } + + /** + * @access private + */ + public function session($instance, $database, $context, array $options = []) + { + return array_rand($this->sessions); + } + + /** + * @access private + */ + public function refreshSessions() + { + // send a request from each session to keep it alive. + } +} diff --git a/dev/src/SetStubConnectionTrait.php b/src/Spanner/Session/SessionPoolInterface.php similarity index 66% rename from dev/src/SetStubConnectionTrait.php rename to src/Spanner/Session/SessionPoolInterface.php index 3ffb3d5b606a..2aa4943d981c 100644 --- a/dev/src/SetStubConnectionTrait.php +++ b/src/Spanner/Session/SessionPoolInterface.php @@ -15,12 +15,18 @@ * limitations under the License. */ -namespace Google\Cloud\Dev; +namespace Google\Cloud\Spanner\Session; -trait SetStubConnectionTrait +/** + * Describes a session pool. + */ +interface SessionPoolInterface { - public function setConnection($conn) - { - $this->connection = $conn; - } + const CONTEXT_READ = 'r'; + const CONTEXT_READWRITE = 'rw'; + + /** + * Get a session from the pool + */ + public function session($instance, $database, $context, array $options = []); } diff --git a/src/Spanner/Session/SimpleSessionPool.php b/src/Spanner/Session/SimpleSessionPool.php new file mode 100644 index 000000000000..d1e756f7b9cf --- /dev/null +++ b/src/Spanner/Session/SimpleSessionPool.php @@ -0,0 +1,54 @@ +sessionClient = $sessionClient; + } + + /** + * @access private + */ + public function session($instance, $database, $mode, array $options = []) + { + if (!isset($this->sessions[$instance.$database.$mode])) { + $this->sessions[$instance.$database] = $this->sessionClient->create($instance, $database, $options); + } + + return $this->sessions[$instance.$database]; + } +} diff --git a/src/Spanner/Snapshot.php b/src/Spanner/Snapshot.php new file mode 100644 index 000000000000..09fd1e2e5af3 --- /dev/null +++ b/src/Spanner/Snapshot.php @@ -0,0 +1,146 @@ +connect('my-instance', 'my-database'); + * $snapshot = $database->snapshot(); + * ``` + * + * @method execute() { + * Run a query. + * + * Example: + * ``` + * $result = $snapshot->execute( + * 'SELECT * FROM Users WHERE id = @userId', + * [ + * 'parameters' => [ + * 'userId' => 1 + * ] + * ] + * ); + * ``` + * + * @param string $sql The query string to execute. + * @param array $options [optional] { + * Configuration options. + * + * @type array $parameters A key/value array of Query Parameters, where + * the key is represented in the query string prefixed by a `@` + * symbol. + * } + * @return Result + * } + * @method read() { + * Lookup rows in a table. + * + * Example: + * ``` + * $keySet = new KeySet([ + * 'keys' => [10] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $snapshot->read('Posts', $keySet, $columns); + * ``` + * + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * } + * @return Result + * } + * @method id() { + * Retrieve the Transaction ID. + * + * Example: + * ``` + * $id = $snapshot->id(); + * ``` + * + * @return string + * } + */ +class Snapshot +{ + use TransactionReadTrait; + + /** + * @var Timestamp + */ + private $readTimestamp; + + /** + * @param Operation $operation The Operation instance. + * @param Session $session The session to use for spanner interactions. + * @param string $transactionId The Transaction ID. + * @param Timestamp $readTimestamp [optional] The read timestamp. + */ + public function __construct( + Operation $operation, + Session $session, + $transactionId, + Timestamp $readTimestamp = null + ) { + $this->operation = $operation; + $this->session = $session; + $this->transactionId = $transactionId; + $this->readTimestamp = $readTimestamp; + $this->context = SessionPoolInterface::CONTEXT_READWRITE; + } + + /** + * Retrieve the Read Timestamp. + * + * For snapshot read-only transactions, the read timestamp chosen for the + * transaction. + * + * Example: + * ``` + * $timestamp = $snapshot->readTimestamp(); + * ``` + * + * @return Timestamp + */ + public function readTimestamp() + { + return $this->readTimestamp; + } +} diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php new file mode 100644 index 000000000000..3c4034bfac67 --- /dev/null +++ b/src/Spanner/SpannerClient.php @@ -0,0 +1,550 @@ + [ + self::FULL_CONTROL_SCOPE, + self::ADMIN_SCOPE + ], + 'returnInt64AsObject' => false + ]; + + $this->connection = new Grpc($this->configureAuthentication($config)); + $this->lroConnection = new LongRunningConnection($this->connection); + + $this->sessionClient = new SessionClient($this->connection, $this->projectId); + $this->sessionPool = new SimpleSessionPool($this->sessionClient); + + $this->returnInt64AsObject = $config['returnInt64AsObject']; + + $this->lroCallables = [ + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', + 'callable' => function ($instance) { + $name = InstanceAdminClient::parseInstanceFromInstanceName($instance['name']); + return $this->instance($name, $instance); + } + ], [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata', + 'callable' => function ($database) { + $instanceName = DatabaseAdminClient::parseInstanceFromDatabaseName($database['name']); + $databaseName = DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']); + + $instance = $this->instance($instanceName); + return $instance->database($databaseName); + } + ], [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata', + 'callable' => function ($instance) { + $name = InstanceAdminClient::parseInstanceFromInstanceName($instance['name']); + return $this->instance($name, $instance); + } + ] + ]; + } + + /** + * List all available configurations. + * + * Example: + * ``` + * $configurations = $spanner->configurations(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.ListInstanceConfigsRequest ListInstanceConfigsRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] { + * Configuration Options. + * + * @type int $pageSize Maximum number of results to return per + * request. + * @type int $resultLimit Limit the number of results returned in total. + * **Defaults to** `0` (return all results). + * @type string $pageToken A previously-returned page token used to + * resume the loading of results from a specific point. + * } + * @return ItemIterator + */ + public function configurations(array $options = []) + { + $resultLimit = $this->pluck('resultLimit', $options, false) ?: 0; + + return new ItemIterator( + new PageIterator( + function (array $config) { + $name = InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config['name']); + return $this->configuration($name, $config); + }, + [$this->connection, 'listConfigs'], + ['projectId' => InstanceAdminClient::formatProjectName($this->projectId)] + $options, + [ + 'itemsKey' => 'instanceConfigs', + 'resultLimit' => $resultLimit + ] + ) + ); + } + + /** + * Get a configuration by its name. + * + * NOTE: This method does not execute a service request and does not verify + * the existence of the given configuration. Unless you know with certainty + * that the configuration exists, it is advised that you use + * {@see Google\Cloud\Spanner\Configuration::exists()} to verify existence + * before attempting to use the configuration. + * + * Example: + * ``` + * $configuration = $spanner->configuration($configurationName); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#getinstanceconfigrequest GetInstanceConfigRequest + * @codingStandardsIgnoreEnd + * + * @param string $name The Configuration name. + * @param array $config [optional] The configuration details. + * @return Configuration + */ + public function configuration($name, array $config = []) + { + return new Configuration($this->connection, $this->projectId, $name, $config); + } + + /** + * Create a new instance. + * + * Example: + * ``` + * $operation = $spanner->createInstance($configuration, 'my-instance'); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#createinstancerequest CreateInstanceRequest + * + * @param Configuration $config The configuration to use + * @param string $name The instance name + * @param array $options [optional] { + * Configuration options + * + * @type string $displayName **Defaults to** the value of $name. + * @type int $nodeCount **Defaults to** `1`. + * @type array $labels For more information, see + * [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). + * } + * @return LongRunningOperation + * @codingStandardsIgnoreEnd + */ + public function createInstance(Configuration $config, $name, array $options = []) + { + $options += [ + 'displayName' => $name, + 'nodeCount' => self::DEFAULT_NODE_COUNT, + 'labels' => [], + 'operationName' => null, + ]; + + // This must always be set to CREATING, so overwrite anything else. + $options['state'] = State::CREATING; + + $operation = $this->connection->createInstance([ + 'instanceId' => $name, + 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), + 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) + ] + $options); + + return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables); + } + + /** + * Lazily instantiate an instance. + * + * Example: + * ``` + * $instance = $spanner->instance('my-instance'); + * ``` + * + * @param string $name The instance name + * @return Instance + */ + public function instance($name, array $instance = []) + { + return new Instance( + $this->connection, + $this->sessionPool, + $this->lroConnection, + $this->lroCallables, + $this->projectId, + $name, + $this->returnInt64AsObject, + $instance + ); + } + + /** + * List instances in the project + * + * Example: + * ``` + * $instances = $spanner->instances(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#listinstancesrequest ListInstancesRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] { + * Configuration options + * + * @type string $filter An expression for filtering the results of the + * request. + * @type int $pageSize Maximum number of results to return per + * request. + * @type int $resultLimit Limit the number of results returned in total. + * **Defaults to** `0` (return all results). + * @type string $pageToken A previously-returned page token used to + * resume the loading of results from a specific point. + * } + * @return ItemIterator + */ + public function instances(array $options = []) + { + $options += [ + 'filter' => null + ]; + + $resultLimit = $this->pluck('resultLimit', $options, false); + return new ItemIterator( + new PageIterator( + function (array $instance) { + $name = InstanceAdminClient::parseInstanceFromInstanceName($instance['name']); + return $this->instance($name, $instance); + }, + [$this->connection, 'listInstances'], + ['projectId' => InstanceAdminClient::formatProjectName($this->projectId)] + $options, + [ + 'itemsKey' => 'instances', + 'resultLimit' => $resultLimit + ] + ) + ); + } + + /** + * Connect to a database to run queries or mutations. + * + * Example: + * ``` + * $database = $spanner->connect('my-instance', 'my-application-database'); + * ``` + * + * @param Instance|string $instance The instance object or instance name. + * @param string $name The database name. + * @return Database + */ + public function connect($instance, $name) + { + if (is_string($instance)) { + $instance = $this->instance($instance); + } + + $database = $instance->database($name); + + return $database; + } + + /** + * Create a new KeySet object + * + * Example: + * ``` + * $keySet = $spanner->keySet(); + * ``` + * + * ``` + * // Create a keyset to return all rows in a table. + * $keySet = $spanner->keySet(['all' => true]); + * ``` + * + * @param array $options [optional] { + * Configuration Options + * + * @type array $keys A list of keys + * @type KeyRange[] $ranges A list of key ranges + * @type bool $all Whether to include all keys in a table + * } + * @return KeySet + */ + public function keySet(array $options = []) + { + return new KeySet($options); + } + + /** + * Create a new KeyRange object + * + * Example: + * ``` + * $range = $spanner->keyRange(); + * ``` + * + * ``` + * // Ranges can be created with all data supplied. + * $range = $spanner->keyRange([ + * 'startType' => KeyRange::TYPE_OPEN, + * 'start' => ['Bob'], + * 'endType' => KeyRange::TYPE_OPEN, + * 'end' => ['Jill'] + * ]); + * ``` + * + * @param array $options [optional] { + * Configuration Options. + * + * @type string $startType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $start The key with which to start the range. + * @type string $endType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $end The key with which to end the range. + * } + * @return KeyRange + */ + public function keyRange(array $options = []) + { + return new KeyRange($options); + } + + /** + * Create a Bytes object. + * + * Example: + * ``` + * $bytes = $spanner->bytes('hello world'); + * ``` + * + * @param string|resource|StreamInterface $value The bytes value. + * @return Bytes + */ + public function bytes($bytes) + { + return new Bytes($bytes); + } + + /** + * Create a Date object. + * + * Example: + * ``` + * $date = $spanner->date(new \DateTime('1995-02-04')); + * ``` + * + * @param \DateTimeInterface $value The date value. + * @return Date + */ + public function date(\DateTimeInterface $date) + { + return new Date($date); + } + + /** + * Create a Timestamp object. + * + * Example: + * ``` + * $timestamp = $spanner->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')); + * ``` + * + * @param \DateTimeInterface $value The timestamp value. + * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp. + * @return Timestamp + */ + public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null) + { + return new Timestamp($timestamp, $nanoSeconds); + } + + /** + * Create an Int64 object. This can be used to work with 64 bit integers as + * a string value while on a 32 bit platform. + * + * Example: + * ``` + * $int64 = $spanner->int64('9223372036854775807'); + * ``` + * + * @param string $value + * @return Int64 + */ + public function int64($value) + { + return new Int64($value); + } + + /** + * Create a Duration object. + * + * Example: + * ``` + * $duration = $spanner->duration(100, 00001); + * ``` + * + * @param int $seconds The number of seconds in the duration. + * @param int $nanos [optional] The number of nanoseconds in the duration. + * **Defaults to** `0`. + * @return Duration + */ + public function duration($seconds, $nanos = 0) + { + return new Duration($seconds, $nanos); + } + + /** + * Get the session client + * + * Example: + * ``` + * $sessionClient = $spanner->sessionClient(); + * ``` + * + * @return SessionClient + */ + public function sessionClient() + { + return $this->sessionClient; + } + + /** + * Resume a Long Running Operation + * + * Example: + * ``` + * $operation = $spanner->resumeOperation($operationName); + * ``` + * + * @param string $operationName The Long Running Operation name. + * @return LongRunningOperation + */ + public function resumeOperation($operationName) + { + return $this->lro($this->lroConnection, $operationName, $this->lroCallables); + } +} diff --git a/src/Spanner/Timestamp.php b/src/Spanner/Timestamp.php new file mode 100644 index 000000000000..61ee9014c572 --- /dev/null +++ b/src/Spanner/Timestamp.php @@ -0,0 +1,129 @@ +timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')); + * ``` + * + * ``` + * // Timestamps can be cast to strings. + * echo (string) $timestamp; + * ``` + */ +class Timestamp implements ValueInterface +{ + const FORMAT = 'Y-m-d\TH:i:s.u\Z'; + const FORMAT_INTERPOLATE = 'Y-m-d\TH:i:s.%\s\Z'; + + /** + * @var \DateTimeInterface + */ + private $value; + + /** + * @var int + */ + private $nanoSeconds; + + /** + * @param \DateTimeInterface $value The timestamp value. + * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp. + */ + public function __construct(\DateTimeInterface $value, $nanoSeconds = null) + { + $this->value = $value; + $this->nanoSeconds = $nanoSeconds ?: (int) $this->value->format('u'); + } + + /** + * Get the underlying `\DateTimeInterface` implementation. + * + * Please note that nanosecond precision is not present in this method. + * + * Example: + * ``` + * $dateTime = $timestamp->get(); + * ``` + * + * @return \DateTimeInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * Example: + * ``` + * $type = $timestamp->type(); + * ``` + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_TIMESTAMP; + } + + /** + * Format the value as a string. + * + * This method retains nanosecond precision, if available. + * + * Example: + * ``` + * $value = $timestamp->formatAsString(); + * ``` + * + * @return string + */ + public function formatAsString() + { + $this->value->setTimezone(new \DateTimeZone('UTC')); + $ns = str_pad((string) $this->nanoSeconds, 6, '0', STR_PAD_LEFT); + return sprintf($this->value->format(self::FORMAT_INTERPOLATE), $ns); + } + + /** + * Format the value as a string. + * + * @return string + * @access private + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php new file mode 100644 index 000000000000..957bf91daf6e --- /dev/null +++ b/src/Spanner/Transaction.php @@ -0,0 +1,465 @@ +spanner(); + * + * $database = $spanner->connect('my-instance', 'my-database'); + * + * $database->runTransaction(function (Transaction $t) { + * // do stuff. + * + * $t->commit(); + * }); + * ``` + * + * ``` + * // Get a transaction to manage manually. + * $transaction = $database->transaction(); + * ``` + * + * @method execute() { + * Run a query. + * + * Example: + * ``` + * $result = $transaction->execute( + * 'SELECT * FROM Users WHERE id = @userId', + * [ + * 'parameters' => [ + * 'userId' => 1 + * ] + * ] + * ); + * ``` + * + * @param string $sql The query string to execute. + * @param array $options [optional] { + * Configuration options. + * + * @type array $parameters A key/value array of Query Parameters, where + * the key is represented in the query string prefixed by a `@` + * symbol. + * } + * @return Result + * } + * @method read() { + * Lookup rows in a table. + * + * Example: + * ``` + * $keySet = new KeySet([ + * 'keys' => [10] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $transaction->read('Posts', $keySet, $columns); + * ``` + * + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * } + * @return Result + * } + * @method id() { + * Retrieve the Transaction ID. + * + * Example: + * ``` + * $id = $transaction->id(); + * ``` + * + * @return string + * } + */ +class Transaction +{ + use TransactionReadTrait; + + const STATE_ACTIVE = 0; + const STATE_ROLLED_BACK = 1; + const STATE_COMMITTED = 2; + + /** + * @var array + */ + private $mutations = []; + + /** + * @var int + */ + private $state = self::STATE_ACTIVE; + + /** + * @param Operation $operation The Operation instance. + * @param Session $session The session to use for spanner interactions. + * @param string $transactionId The Transaction ID. + */ + public function __construct( + Operation $operation, + Session $session, + $transactionId + ) { + $this->operation = $operation; + $this->session = $session; + $this->transactionId = $transactionId; + $this->context = SessionPoolInterface::CONTEXT_READWRITE; + } + + /** + * Enqueue an insert mutation. + * + * Example: + * ``` + * $transaction->insert('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ]); + * ``` + * + * @param string $table The table to insert into. + * @param array $data The data to insert. + * @return Transaction The transaction, to enable method chaining. + */ + public function insert($table, array $data) + { + return $this->insertBatch($table, [$data]); + } + + /** + * Enqueue one or more insert mutations. + * + * Example: + * ``` + * $transaction->insertBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ] + * ]); + * ``` + * + * @param string $table The table to insert into. + * @param array $dataSet The data to insert. + * @return Transaction The transaction, to enable method chaining. + */ + public function insertBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_INSERT, $table, $dataSet); + + return $this; + } + + /** + * Enqueue an update mutation. + * + * Example: + * ``` + * $transaction->update('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post [Updated!]', + * 'content' => 'Modified Content' + * ]); + * ``` + * + * @param string $table The table to update. + * @param array $data The data to update. + * @return Transaction The transaction, to enable method chaining. + */ + public function update($table, array $data) + { + return $this->updateBatch($table, [$data]); + } + + /** + * Enqueue one or more update mutations. + * + * Example: + * ``` + * $transaction->updateBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post [Updated!]', + * 'content' => 'Modified Content' + * ] + * ]); + * ``` + * + * @param string $table The table to update. + * @param array $dataSet The data to update. + * @return Transaction The transaction, to enable method chaining. + */ + public function updateBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_UPDATE, $table, $dataSet); + + return $this; + } + + /** + * Enqueue an insert or update mutation. + * + * Example: + * ``` + * $transaction->insertOrUpdate('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ]); + * ``` + * + * @param string $table The table to insert into or update. + * @param array $data The data to insert or update. + * @return Transaction The transaction, to enable method chaining. + */ + public function insertOrUpdate($table, array $data) + { + return $this->insertOrUpdateBatch($table, [$data]); + } + + /** + * Enqueue one or more insert or update mutations. + * + * Example: + * ``` + * $transaction->insertOrUpdateBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ] + * ]); + * ``` + * + * @param string $table The table to insert into or update. + * @param array $dataSet The data to insert or update. + * @return Transaction The transaction, to enable method chaining. + */ + public function insertOrUpdateBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_INSERT_OR_UPDATE, $table, $dataSet); + + return $this; + } + + /** + * Enqueue an replace mutation. + * + * Example: + * ``` + * $transaction->replace('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post [Replaced]', + * 'content' => 'Hello Moon' + * ]); + * ``` + * + * @param string $table The table to replace into. + * @param array $data The data to replace. + * @return Transaction The transaction, to enable method chaining. + */ + public function replace($table, array $data) + { + return $this->replaceBatch($table, [$data]); + } + + /** + * Enqueue one or more replace mutations. + * + * Example: + * ``` + * $transaction->replaceBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post [Replaced]', + * 'content' => 'Hello Moon' + * ] + * ]); + * ``` + * + * @param string $table The table to replace into. + * @param array $dataSet The data to replace. + * @return Transaction The transaction, to enable method chaining. + */ + public function replaceBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_REPLACE, $table, $dataSet); + + return $this; + } + + /** + * Enqueue an delete mutation. + * + * Example: + * ``` + * $keySet = new KeySet([ + * 'keys' => [10] + * ]); + * + * $transaction->delete('Posts', $keySet); + * ``` + * + * @param string $table The table to mutate. + * @param KeySet $keySet The KeySet to identify rows to delete. + * @return Transaction The transaction, to enable method chaining. + */ + public function delete($table, KeySet $keySet) + { + $this->enqueue(Operation::OP_DELETE, $table, [$keySet]); + + return $this; + } + + /** + * Roll back a transaction. + * + * Rolls back a transaction, releasing any locks it holds. It is a good idea + * to call this for any transaction that includes one or more Read or + * ExecuteSql requests and ultimately decides not to commit. + * + * This closes the transaction, preventing any future API calls inside it. + * + * Rollback will NOT error if the transaction is not found or was already aborted. + * + * Example: + * ``` + * $transaction->rollback(); + * ``` + * + * @param array $options [optional] Configuration Options. + * @return void + */ + public function rollback(array $options = []) + { + if ($this->state !== self::STATE_ACTIVE) { + throw new \RuntimeException('The transaction cannot be rolled back because it is not active'); + } + + $this->state = self::STATE_ROLLED_BACK; + + return $this->operation->rollback($this->session, $this->transactionId, $options); + } + + /** + * Commit and end the transaction. + * + * It is advised that transactions be run inside + * {@see Google\Cloud\Spanner\Database::runTransaction()} in order to take + * advantage of automated transaction retry in case of a transaction aborted + * error. + * + * Example: + * ``` + * $transaction->commit(); + * ``` + * + * @param array $options [optional] Configuration Options. + * @return Timestamp The commit timestamp. + * @throws \RuntimeException If the transaction is not active + * @throws \AbortedException If the commit is aborted for any reason. + */ + public function commit(array $options = []) + { + if ($this->state !== self::STATE_ACTIVE) { + throw new \RuntimeException('The transaction cannot be committed because it is not active'); + } + + $this->state = self::STATE_COMMITTED; + + $options['transactionId'] = $this->transactionId; + return $this->operation->commit($this->session, $this->mutations, $options); + } + + /** + * Retrieve the Transaction State. + * + * Will be one of `Transaction::STATE_ACTIVE`, + * `Transaction::STATE_COMMITTED`, or `Transaction::STATE_ROLLED_BACK`. + * + * Example: + * ``` + * $state = $transaction->state(); + * ``` + * + * @return int + */ + public function state() + { + return $this->state; + } + + /** + * Format, validate and enqueue mutations in the transaction. + * + * @param string $op The operation type. + * @param string $table The table name + * @param array $dataSet the mutations to enqueue + * @return void + */ + private function enqueue($op, $table, array $dataSet) + { + foreach ($dataSet as $data) { + if ($op === Operation::OP_DELETE) { + $this->mutations[] = $this->operation->deleteMutation($table, $data); + } else { + $this->mutations[] = $this->operation->mutation($op, $table, $data); + } + } + } +} diff --git a/src/Spanner/TransactionConfigurationTrait.php b/src/Spanner/TransactionConfigurationTrait.php new file mode 100644 index 000000000000..a5f5e61b4c61 --- /dev/null +++ b/src/Spanner/TransactionConfigurationTrait.php @@ -0,0 +1,155 @@ + false, + 'transactionType' => SessionPoolInterface::CONTEXT_READ, + 'transactionId' => null + ]; + + $type = null; + + $context = $this->pluck('transactionType', $options); + $id = $this->pluck('transactionId', $options); + if (!is_null($id)) { + $type = 'id'; + $transactionOptions = $id; + } elseif ($context === SessionPoolInterface::CONTEXT_READ) { + $transactionOptions = $this->configureSnapshotOptions($options); + } elseif ($context === SessionPoolInterface::CONTEXT_READWRITE) { + $transactionOptions = $this->configureTransactionOptions(); + } else { + throw new \BadMethodCallException(sprintf( + 'Invalid transaction context %s', + $context + )); + } + + $begin = $this->pluck('begin', $options); + if (is_null($type)) { + $type = ($begin) ? 'begin' : 'singleUse'; + } + + return [ + [$type => $transactionOptions], + $context + ]; + } + + private function configureTransactionOptions() + { + return [ + 'readWrite' => [] + ]; + } + + /** + * Create a Read Only single use transaction. + * + * @param array $options Configuration Options. + * @return array + */ + private function configureSnapshotOptions(array &$options) + { + $options += [ + 'returnReadTimestamp' => null, + 'strong' => null, + 'readTimestamp' => null, + 'exactStaleness' => null, + 'minReadTimestamp' => null, + 'maxStaleness' => null, + ]; + + $transactionOptions = [ + 'readOnly' => $this->arrayFilterRemoveNull([ + 'returnReadTimestamp' => $this->pluck('returnReadTimestamp', $options), + 'strong' => $this->pluck('strong', $options), + 'minReadTimestamp' => $this->pluck('minReadTimestamp', $options), + 'maxStaleness' => $this->pluck('maxStaleness', $options), + 'readTimestamp' => $this->pluck('readTimestamp', $options), + 'exactStaleness' => $this->pluck('exactStaleness', $options), + ]) + ]; + + if (empty($transactionOptions['readOnly'])) { + $transactionOptions['readOnly']['strong'] = true; + } + + $timestampFields = [ + 'minReadTimestamp', + 'readTimestamp' + ]; + + $durationFields = [ + 'exactStaleness', + 'maxStaleness' + ]; + + foreach ($timestampFields as $tsf) { + if (isset($transactionOptions['readOnly'][$tsf])) { + $field = $transactionOptions['readOnly'][$tsf]; + if (!($field instanceof Timestamp)) { + throw new \BadMethodCallException(sprintf( + 'Read Only Transaction Configuration Field %s must be an instance of Timestamp', + $tsf + )); + } + + $transactionOptions['readOnly'][$tsf] = $field->formatAsString(); + } + } + + foreach ($durationFields as $df) { + if (isset($transactionOptions['readOnly'][$df])) { + $field = $transactionOptions['readOnly'][$df]; + if (!($field instanceof Duration)) { + throw new \BadMethodCallException(sprintf( + 'Read Only Transaction Configuration Field %s must be an instance of Duration', + $df + )); + } + + $transactionOptions['readOnly'][$df] = $field->get(); + } + } + + return $transactionOptions; + } +} diff --git a/src/Spanner/TransactionReadTrait.php b/src/Spanner/TransactionReadTrait.php new file mode 100644 index 000000000000..b98b41f6725c --- /dev/null +++ b/src/Spanner/TransactionReadTrait.php @@ -0,0 +1,108 @@ +context; + $options['transactionId'] = $this->transactionId; + + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; + $options['transactionContext'] = $context; + + return $this->operation->execute($this->session, $sql, $options); + } + + /** + * Lookup rows in a table. + * + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * } + * @return Result + */ + public function read($table, KeySet $keySet, array $columns, array $options = []) + { + $options['transactionType'] = $this->context; + $options['transactionId'] = $this->transactionId; + + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; + $options['transactionContext'] = $context; + + return $this->operation->read($this->session, $table, $keySet, $columns, $options); + } + + /** + * Retrieve the Transaction ID. + * + * @return string + */ + public function id() + { + return $this->transactionId; + } +} diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php new file mode 100644 index 000000000000..a88419ad1441 --- /dev/null +++ b/src/Spanner/V1/SpannerClient.php @@ -0,0 +1,1180 @@ +createSession($formattedDatabase); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class SpannerClient +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'spanner.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; + + private static $databaseNameTemplate; + private static $sessionNameTemplate; + + private $grpcCredentialsHelper; + private $spannerStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + + /** + * Formats a string containing the fully-qualified path to represent + * a database resource. + */ + public static function formatDatabaseName($project, $instance, $database) + { + return self::getDatabaseNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a session resource. + */ + public static function formatSessionName($project, $instance, $database, $session) + { + return self::getSessionNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + 'session' => $session, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a database resource. + */ + public static function parseProjectFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a database resource. + */ + public static function parseInstanceFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a database resource. + */ + public static function parseDatabaseFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['database']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a session resource. + */ + public static function parseProjectFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a session resource. + */ + public static function parseInstanceFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a session resource. + */ + public static function parseDatabaseFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['database']; + } + + /** + * Parses the session from the given fully-qualified path which + * represents a session resource. + */ + public static function parseSessionFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['session']; + } + + private static function getDatabaseNameTemplate() + { + if (self::$databaseNameTemplate == null) { + self::$databaseNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}'); + } + + return self::$databaseNameTemplate; + } + + private static function getSessionNameTemplate() + { + if (self::$sessionNameTemplate == null) { + self::$sessionNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}/sessions/{session}'); + } + + return self::$sessionNameTemplate; + } + + private static function getGrpcStreamingDescriptors() + { + return [ + 'executeStreamingSql' => [ + 'grpcStreamingType' => 'ServerStreaming', + ], + 'streamingRead' => [ + 'grpcStreamingType' => 'ServerStreaming', + ], + ]; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'spanner.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type \Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * \Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type \Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.data', + ], + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + ]; + $options = array_merge($defaultOptions, $options); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, + 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'createSession' => $defaultDescriptors, + 'getSession' => $defaultDescriptors, + 'deleteSession' => $defaultDescriptors, + 'executeSql' => $defaultDescriptors, + 'executeStreamingSql' => $defaultDescriptors, + 'read' => $defaultDescriptors, + 'streamingRead' => $defaultDescriptors, + 'beginTransaction' => $defaultDescriptors, + 'commit' => $defaultDescriptors, + 'rollback' => $defaultDescriptors, + ]; + $grpcStreamingDescriptors = self::getGrpcStreamingDescriptors(); + foreach ($grpcStreamingDescriptors as $method => $grpcStreamingDescriptor) { + $this->descriptors[$method]['grpcStreamingDescriptor'] = $grpcStreamingDescriptor; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/spanner_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.v1.Spanner', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (array_key_exists('sslCreds', $options)) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createSpannerStubFunction = function ($hostname, $opts) { + return new SpannerGrpcClient($hostname, $opts); + }; + if (array_key_exists('createSpannerStubFunction', $options)) { + $createSpannerStubFunction = $options['createSpannerStubFunction']; + } + $this->spannerStub = $this->grpcCredentialsHelper->createStub( + $createSpannerStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Creates a new session. A session can be used to perform + * transactions that read and/or modify data in a Cloud Spanner database. + * Sessions are meant to be reused for many consecutive + * transactions. + * + * Sessions can only execute one transaction at a time. To execute + * multiple concurrent read-write/write-only transactions, create + * multiple sessions. Note that standalone reads and queries use a + * transaction internally, and count toward the one transaction + * limit. + * + * Cloud Spanner limits the number of sessions that can exist at any given + * time; thus, it is a good idea to delete idle and/or unneeded sessions. + * Aside from explicit deletes, Cloud Spanner can delete sessions for + * which no operations are sent for more than an hour, or due to + * internal errors. If a session is deleted, requests to it + * return `NOT_FOUND`. + * + * Idle sessions can be kept alive by sending a trivial SQL query + * periodically, e.g., `"SELECT 1"`. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedDatabase = SpannerClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $spannerClient->createSession($formattedDatabase); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $database Required. The database in which the new session is created. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\v1\Session + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function createSession($database, $optionalArgs = []) + { + $request = new CreateSessionRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['createSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'CreateSession', + $mergedSettings, + $this->descriptors['createSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets a session. Returns `NOT_FOUND` if the session does not exist. + * This is mainly useful for determining whether a session is still + * alive. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $response = $spannerClient->getSession($formattedName); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $name Required. The name of the session to retrieve. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\v1\Session + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getSession($name, $optionalArgs = []) + { + $request = new GetSessionRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'GetSession', + $mergedSettings, + $this->descriptors['getSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Ends a session, releasing server resources associated with it. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerClient->deleteSession($formattedName); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $name Required. The name of the session to delete. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function deleteSession($name, $optionalArgs = []) + { + $request = new DeleteSessionRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['deleteSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'DeleteSession', + $mergedSettings, + $this->descriptors['deleteSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Executes an SQL query, returning all rows in a single reply. This + * method cannot be used to return a result set larger than 10 MiB; + * if the query yields more data than that, the query fails with + * a `FAILED_PRECONDITION` error. + * + * Queries inside read-write transactions might return `ABORTED`. If + * this occurs, the application should restart the transaction from + * the beginning. See [Transaction][google.spanner.v1.Transaction] for more details. + * + * Larger result sets can be fetched in streaming fashion by calling + * [ExecuteStreamingSql][google.spanner.v1.Spanner.ExecuteStreamingSql] instead. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $sql = ""; + * $response = $spannerClient->executeSql($formattedSession, $sql); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the SQL query should be performed. + * @param string $sql Required. The SQL query string. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type Struct $params + * The SQL query string can contain parameter placeholders. A parameter + * placeholder consists of `'@'` followed by the parameter + * name. Parameter names consist of any combination of letters, + * numbers, and underscores. + * + * Parameters can appear anywhere that a literal value is expected. The same + * parameter name can be used more than once, for example: + * `"WHERE id > @msg_id AND id < @msg_id + 100"` + * + * It is an error to execute an SQL query with unbound parameters. + * + * Parameter values are specified using `params`, which is a JSON + * object whose keys are parameter names, and whose values are the + * corresponding parameter values. + * @type array $paramTypes + * It is not always possible for Cloud Spanner to infer the right SQL type + * from a JSON value. For example, values of type `BYTES` and values + * of type `STRING` both appear in [params][google.spanner.v1.ExecuteSqlRequest.params] as JSON strings. + * + * In these cases, `param_types` can be used to specify the exact + * SQL type for some or all of the SQL query parameters. See the + * definition of [Type][google.spanner.v1.Type] for more information + * about SQL types. + * @type string $resumeToken + * If this request is resuming a previously interrupted SQL query + * execution, `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new SQL query execution to resume where the last one left + * off. The rest of the request parameters must exactly match the + * request that yielded this token. + * @type QueryMode $queryMode + * Used to control the amount of debugging information returned in + * [ResultSetStats][google.spanner.v1.ResultSetStats]. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\v1\ResultSet + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function executeSql($session, $sql, $optionalArgs = []) + { + $request = new ExecuteSqlRequest(); + $request->setSession($session); + $request->setSql($sql); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['params'])) { + $request->setParams($optionalArgs['params']); + } + if (isset($optionalArgs['paramTypes'])) { + foreach ($optionalArgs['paramTypes'] as $key => $value) { + $request->addParamTypes((new ParamTypesEntry())->setKey($key)->setValue($value)); + } + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + if (isset($optionalArgs['queryMode'])) { + $request->setQueryMode($optionalArgs['queryMode']); + } + + $mergedSettings = $this->defaultCallSettings['executeSql']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'ExecuteSql', + $mergedSettings, + $this->descriptors['executeSql'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Like [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], except returns the result + * set as a stream. Unlike [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], there + * is no limit on the size of the returned result set. However, no + * individual row in the result set can exceed 100 MiB, and no + * column value can exceed 10 MiB. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $sql = ""; + * // Read all responses until the stream is complete + * $stream = $spannerClient->executeStreamingSql($formattedSession, $sql); + * foreach ($stream->readAll() as $element) { + * // doSomethingWith($element); + * } + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the SQL query should be performed. + * @param string $sql Required. The SQL query string. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type Struct $params + * The SQL query string can contain parameter placeholders. A parameter + * placeholder consists of `'@'` followed by the parameter + * name. Parameter names consist of any combination of letters, + * numbers, and underscores. + * + * Parameters can appear anywhere that a literal value is expected. The same + * parameter name can be used more than once, for example: + * `"WHERE id > @msg_id AND id < @msg_id + 100"` + * + * It is an error to execute an SQL query with unbound parameters. + * + * Parameter values are specified using `params`, which is a JSON + * object whose keys are parameter names, and whose values are the + * corresponding parameter values. + * @type array $paramTypes + * It is not always possible for Cloud Spanner to infer the right SQL type + * from a JSON value. For example, values of type `BYTES` and values + * of type `STRING` both appear in [params][google.spanner.v1.ExecuteSqlRequest.params] as JSON strings. + * + * In these cases, `param_types` can be used to specify the exact + * SQL type for some or all of the SQL query parameters. See the + * definition of [Type][google.spanner.v1.Type] for more information + * about SQL types. + * @type string $resumeToken + * If this request is resuming a previously interrupted SQL query + * execution, `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new SQL query execution to resume where the last one left + * off. The rest of the request parameters must exactly match the + * request that yielded this token. + * @type QueryMode $queryMode + * Used to control the amount of debugging information returned in + * [ResultSetStats][google.spanner.v1.ResultSetStats]. + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * + * @return \Google\GAX\ServerStreamingResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function executeStreamingSql($session, $sql, $optionalArgs = []) + { + $request = new ExecuteSqlRequest(); + $request->setSession($session); + $request->setSql($sql); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['params'])) { + $request->setParams($optionalArgs['params']); + } + if (isset($optionalArgs['paramTypes'])) { + foreach ($optionalArgs['paramTypes'] as $key => $value) { + $request->addParamTypes((new ParamTypesEntry())->setKey($key)->setValue($value)); + } + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + if (isset($optionalArgs['queryMode'])) { + $request->setQueryMode($optionalArgs['queryMode']); + } + + $mergedSettings = $this->defaultCallSettings['executeStreamingSql']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'ExecuteStreamingSql', + $mergedSettings, + $this->descriptors['executeStreamingSql'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Reads rows from the database using key lookups and scans, as a + * simple key/value style alternative to + * [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql]. This method cannot be used to + * return a result set larger than 10 MiB; if the read matches more + * data than that, the read fails with a `FAILED_PRECONDITION` + * error. + * + * Reads inside read-write transactions might return `ABORTED`. If + * this occurs, the application should restart the transaction from + * the beginning. See [Transaction][google.spanner.v1.Transaction] for more details. + * + * Larger result sets can be yielded in streaming fashion by calling + * [StreamingRead][google.spanner.v1.Spanner.StreamingRead] instead. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $table = ""; + * $columns = []; + * $keySet = new KeySet(); + * $response = $spannerClient->read($formattedSession, $table, $columns, $keySet); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the read should be performed. + * @param string $table Required. The name of the table in the database to be read. + * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching + * this request. + * @param KeySet $keySet Required. `key_set` identifies the rows to be yielded. `key_set` names the + * primary keys of the rows in [table][google.spanner.v1.ReadRequest.table] to be yielded, unless [index][google.spanner.v1.ReadRequest.index] + * is present. If [index][google.spanner.v1.ReadRequest.index] is present, then [key_set][google.spanner.v1.ReadRequest.key_set] instead names + * index keys in [index][google.spanner.v1.ReadRequest.index]. + * + * Rows are yielded in table primary key order (if [index][google.spanner.v1.ReadRequest.index] is empty) + * or index key order (if [index][google.spanner.v1.ReadRequest.index] is non-empty). + * + * It is not an error for the `key_set` to name rows that do not + * exist in the database. Read yields nothing for nonexistent rows. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type string $index + * If non-empty, the name of an index on [table][google.spanner.v1.ReadRequest.table]. This index is + * used instead of the table primary key when interpreting [key_set][google.spanner.v1.ReadRequest.key_set] + * and sorting result rows. See [key_set][google.spanner.v1.ReadRequest.key_set] for further information. + * @type int $limit + * If greater than zero, only the first `limit` rows are yielded. If `limit` + * is zero, the default is no limit. + * @type string $resumeToken + * If this request is resuming a previously interrupted read, + * `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new read to resume where the last read left off. The + * rest of the request parameters must exactly match the request + * that yielded this token. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\v1\ResultSet + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function read($session, $table, $columns, $keySet, $optionalArgs = []) + { + $request = new ReadRequest(); + $request->setSession($session); + $request->setTable($table); + foreach ($columns as $elem) { + $request->addColumns($elem); + } + $request->setKeySet($keySet); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['index'])) { + $request->setIndex($optionalArgs['index']); + } + if (isset($optionalArgs['limit'])) { + $request->setLimit($optionalArgs['limit']); + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + + $mergedSettings = $this->defaultCallSettings['read']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Read', + $mergedSettings, + $this->descriptors['read'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Like [Read][google.spanner.v1.Spanner.Read], except returns the result set as a + * stream. Unlike [Read][google.spanner.v1.Spanner.Read], there is no limit on the + * size of the returned result set. However, no individual row in + * the result set can exceed 100 MiB, and no column value can exceed + * 10 MiB. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $table = ""; + * $columns = []; + * $keySet = new KeySet(); + * // Read all responses until the stream is complete + * $stream = $spannerClient->streamingRead($formattedSession, $table, $columns, $keySet); + * foreach ($stream->readAll() as $element) { + * // doSomethingWith($element); + * } + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the read should be performed. + * @param string $table Required. The name of the table in the database to be read. + * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching + * this request. + * @param KeySet $keySet Required. `key_set` identifies the rows to be yielded. `key_set` names the + * primary keys of the rows in [table][google.spanner.v1.ReadRequest.table] to be yielded, unless [index][google.spanner.v1.ReadRequest.index] + * is present. If [index][google.spanner.v1.ReadRequest.index] is present, then [key_set][google.spanner.v1.ReadRequest.key_set] instead names + * index keys in [index][google.spanner.v1.ReadRequest.index]. + * + * Rows are yielded in table primary key order (if [index][google.spanner.v1.ReadRequest.index] is empty) + * or index key order (if [index][google.spanner.v1.ReadRequest.index] is non-empty). + * + * It is not an error for the `key_set` to name rows that do not + * exist in the database. Read yields nothing for nonexistent rows. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type string $index + * If non-empty, the name of an index on [table][google.spanner.v1.ReadRequest.table]. This index is + * used instead of the table primary key when interpreting [key_set][google.spanner.v1.ReadRequest.key_set] + * and sorting result rows. See [key_set][google.spanner.v1.ReadRequest.key_set] for further information. + * @type int $limit + * If greater than zero, only the first `limit` rows are yielded. If `limit` + * is zero, the default is no limit. + * @type string $resumeToken + * If this request is resuming a previously interrupted read, + * `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new read to resume where the last read left off. The + * rest of the request parameters must exactly match the request + * that yielded this token. + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * + * @return \Google\GAX\ServerStreamingResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function streamingRead($session, $table, $columns, $keySet, $optionalArgs = []) + { + $request = new ReadRequest(); + $request->setSession($session); + $request->setTable($table); + foreach ($columns as $elem) { + $request->addColumns($elem); + } + $request->setKeySet($keySet); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['index'])) { + $request->setIndex($optionalArgs['index']); + } + if (isset($optionalArgs['limit'])) { + $request->setLimit($optionalArgs['limit']); + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + + $mergedSettings = $this->defaultCallSettings['streamingRead']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'StreamingRead', + $mergedSettings, + $this->descriptors['streamingRead'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Begins a new transaction. This step can often be skipped: + * [Read][google.spanner.v1.Spanner.Read], [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] and + * [Commit][google.spanner.v1.Spanner.Commit] can begin a new transaction as a + * side-effect. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $options = new TransactionOptions(); + * $response = $spannerClient->beginTransaction($formattedSession, $options); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the transaction runs. + * @param TransactionOptions $options Required. Options for the new transaction. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\v1\Transaction + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function beginTransaction($session, $options, $optionalArgs = []) + { + $request = new BeginTransactionRequest(); + $request->setSession($session); + $request->setOptions($options); + + $mergedSettings = $this->defaultCallSettings['beginTransaction']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'BeginTransaction', + $mergedSettings, + $this->descriptors['beginTransaction'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Commits a transaction. The request includes the mutations to be + * applied to rows in the database. + * + * `Commit` might return an `ABORTED` error. This can occur at any time; + * commonly, the cause is conflicts with concurrent + * transactions. However, it can also happen for a variety of other + * reasons. If `Commit` returns `ABORTED`, the caller should re-attempt + * the transaction from the beginning, re-using the same session. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $mutations = []; + * $response = $spannerClient->commit($formattedSession, $mutations); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the transaction to be committed is running. + * @param Mutation[] $mutations The mutations to be executed when this transaction commits. All + * mutations are applied atomically, in the order they appear in + * this list. + * @param array $optionalArgs { + * Optional. + * + * @type string $transactionId + * Commit a previously-started transaction. + * @type TransactionOptions $singleUseTransaction + * Execute mutations in a temporary transaction. Note that unlike + * commit of a previously-started transaction, commit with a + * temporary transaction is non-idempotent. That is, if the + * `CommitRequest` is sent to Cloud Spanner more than once (for + * instance, due to retries in the application, or in the + * transport library), it is possible that the mutations are + * executed more than once. If this is undesirable, use + * [BeginTransaction][google.spanner.v1.Spanner.BeginTransaction] and + * [Commit][google.spanner.v1.Spanner.Commit] instead. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\v1\CommitResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function commit($session, $mutations, $optionalArgs = []) + { + $request = new CommitRequest(); + $request->setSession($session); + foreach ($mutations as $elem) { + $request->addMutations($elem); + } + if (isset($optionalArgs['transactionId'])) { + $request->setTransactionId($optionalArgs['transactionId']); + } + if (isset($optionalArgs['singleUseTransaction'])) { + $request->setSingleUseTransaction($optionalArgs['singleUseTransaction']); + } + + $mergedSettings = $this->defaultCallSettings['commit']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Commit', + $mergedSettings, + $this->descriptors['commit'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Rolls back a transaction, releasing any locks it holds. It is a good + * idea to call this for any transaction that includes one or more + * [Read][google.spanner.v1.Spanner.Read] or [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] requests and + * ultimately decides not to commit. + * + * `Rollback` returns `OK` if it successfully aborts the transaction, the + * transaction was already aborted, or the transaction is not + * found. `Rollback` never returns `ABORTED`. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $transactionId = ""; + * $spannerClient->rollback($formattedSession, $transactionId); + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the transaction to roll back is running. + * @param string $transactionId Required. The transaction to roll back. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function rollback($session, $transactionId, $optionalArgs = []) + { + $request = new RollbackRequest(); + $request->setSession($session); + $request->setTransactionId($transactionId); + + $mergedSettings = $this->defaultCallSettings['rollback']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Rollback', + $mergedSettings, + $this->descriptors['rollback'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->spannerStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/V1/resources/spanner_client_config.json b/src/Spanner/V1/resources/spanner_client_config.json new file mode 100644 index 000000000000..db4ced68c440 --- /dev/null +++ b/src/Spanner/V1/resources/spanner_client_config.json @@ -0,0 +1,78 @@ +{ + "interfaces": { + "google.spanner.v1.Spanner": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 1000, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 32000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "CreateSession": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetSession": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DeleteSession": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "ExecuteSql": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "ExecuteStreamingSql": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Read": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "StreamingRead": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "BeginTransaction": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Commit": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Rollback": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/Spanner/ValueInterface.php b/src/Spanner/ValueInterface.php new file mode 100644 index 000000000000..e5e140d1c751 --- /dev/null +++ b/src/Spanner/ValueInterface.php @@ -0,0 +1,44 @@ +returnInt64AsObject = $returnInt64AsObject; + } + + /** + * Accepts an array of key/value pairs, where the key is a SQL parameter + * name and the value is the value interpolated by the server, and returns + * an array of parameters and inferred parameter types. + * + * @param array $parameters The key/value parameters. + * @return array An associative array containing params and paramTypes. + */ + public function formatParamsForExecuteSql(array $parameters) + { + $paramTypes = []; + + foreach ($parameters as $key => $value) { + list ($parameters[$key], $paramTypes[$key]) = $this->paramType($value); + } + + return [ + 'params' => $parameters, + 'paramTypes' => $paramTypes + ]; + } + + /** + * Accepts a list of values and encodes the value into a format accepted by + * the Spanner API. + * + * @param array $values The list of values + * @return array The encoded values + */ + public function encodeValuesAsSimpleType(array $values) + { + $res = []; + foreach ($values as $value) { + $res[] = $this->paramType($value)[0]; + } + + return $res; + } + + /** + * Accepts a list of columns (with name and type) and a row from read or + * executeSql and decodes each value to its corresponding PHP type. + * + * @param array $columns The list of columns + * @param array $row The row data. + * @return array The decoded row data. + */ + public function decodeValues(array $columns, array $row, $extractResult = false) + { + $cols = []; + $types = []; + + foreach ($columns as $index => $column) { + $cols[] = (isset($column['name'])) + ? $column['name'] + : $index; + $types[] = $column['type']; + } + + $res = []; + foreach ($row as $index => $value) { + $i = $cols[$index]; + $res[$i] = $this->decodeValue($value, $types[$index]); + } + + return $res; + } + + /** + * Convert a timestamp string to a Timestamp class with nanosecond support. + * + * @param string $timestamp The timestamp string + * @return Timestamp + */ + public function createTimestampWithNanos($timestamp) + { + $matches = []; + preg_match(self::NANO_REGEX, $timestamp, $matches); + $timestamp = preg_replace(self::NANO_REGEX, '.000000Z', $timestamp); + + $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $timestamp); + return new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0); + } + + /** + * Convert a single value to its corresponding PHP type. + * + * @param mixed $value The value to decode + * @param array $type The value type + * @return mixed + */ + private function decodeValue($value, array $type) + { + switch ($type['code']) { + case self::TYPE_INT64: + $value = $this->returnInt64AsObject + ? new Int64($value) + : (int) $value; + break; + + case self::TYPE_TIMESTAMP: + $value = $this->createTimestampWithNanos($value); + break; + + case self::TYPE_DATE: + $value = new Date(new \DateTimeImmutable($value)); + break; + + case self::TYPE_BYTES: + $value = new Bytes(base64_decode($value)); + break; + + case self::TYPE_ARRAY: + $res = []; + foreach ($value as $item) { + $res[] = $this->decodeValue($item, $type['arrayElementType']); + } + + $value = $res; + break; + + case self::TYPE_STRUCT: + $value = $this->decodeValues($type['structType']['fields'], $value, true); + break; + + case self::TYPE_FLOAT64: + // NaN, Infinite and -Infinite are possible FLOAT64 values, + // but when the gRPC response is decoded, they are represented + // as strings. This conditional checks for a string, converts to + // an equivalent double value, or dies if something really weird + // happens. + if (is_string($value)) { + switch ($value) { + case 'NaN': + $value = NAN; + break; + + case 'Infinity': + $value = INF; + break; + + case '-Infinity': + $value = -INF; + break; + + default: + throw new \RuntimeException(sprintf( + 'Unexpected string value %s encountered in FLOAT64 field.', + $value + )); + } + } + + break; + } + + return $value; + } + + /** + * Create a spanner parameter type value object from a PHP value type. + * + * @param mixed $value The PHP value + * @return array The Value type + */ + private function paramType($value) + { + $phpType = gettype($value); + switch ($phpType) { + case 'boolean': + $type = $this->typeObject(self::TYPE_BOOL); + break; + + case 'integer': + $value = (string) $value; + $type = $this->typeObject(self::TYPE_INT64); + break; + + case 'double': + $type = $this->typeObject(self::TYPE_FLOAT64); + break; + + case 'string': + $type = $this->typeObject(self::TYPE_STRING); + break; + + case 'resource': + $type = $this->typeObject(self::TYPE_BYTES); + $value = base64_encode(stream_get_contents($value)); + break; + + case 'object': + list ($type, $value) = $this->objectParam($value); + break; + + case 'array': + if ($this->isAssoc($value)) { + throw new \InvalidArgumentException( + 'Associative arrays are not supported. Did you mean to call a batch method?' + ); + } + + $res = []; + $types = []; + foreach ($value as $element) { + $type = $this->paramType($element); + $res[] = $type[0]; + $types[] = $type[1]['code']; + } + + if (count(array_unique($types)) !== 1) { + throw new \InvalidArgumentException('Array values may not be of mixed type'); + } + + $type = $this->typeObject( + self::TYPE_ARRAY, + $this->typeObject($types[0]), + 'arrayElementType' + ); + + $value = $res; + break; + + case 'NULL': + $type = null; + break; + + default: + throw new \InvalidArgumentException(sprintf( + 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.', + $phpType + )); + break; + } + + return [$value, $type]; + } + + private function objectParam($value) + { + if ($value instanceof ValueInterface) { + return [ + $this->typeObject($value->type()), + $value->formatAsString() + ]; + } + + if ($value instanceof Int64) { + return [ + $this->typeObject(self::TYPE_INT64), + $value->get() + ]; + } + + throw new \InvalidArgumentException(sprintf( + 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.', + get_class($value) + )); + } + + private function typeObject($type, array $nestedDefinition = [], $nestedDefinitionType = null) + { + return array_filter([ + 'code' => $type, + $nestedDefinitionType => $nestedDefinition + ]); + } +} diff --git a/src/Spanner/composer.json b/src/Spanner/composer.json new file mode 100644 index 000000000000..c71551886e92 --- /dev/null +++ b/src/Spanner/composer.json @@ -0,0 +1,26 @@ +{ + "name": "google/cloud-spanner", + "description": "Cloud Spanner Client for PHP", + "license": "Apache-2.0", + "minimum-stability": "stable", + "require": { + "google/cloud-core": "*" + }, + "suggest": { + "google/gax": "Required to support gRPC", + "google/proto-client-php": "Required to support gRPC" + }, + "extra": { + "component": { + "id": "cloud-spanner", + "target": "GoogleCloudPlatform/google-cloud-php-spanner.git", + "path": "src/Spanner", + "entry": "SpannerClient.php" + } + }, + "autoload": { + "psr-4": { + "Google\\Cloud\\Spanner\\": "" + } + } +} diff --git a/tests/snippets/BigQuery/BigQueryClientTest.php b/tests/snippets/BigQuery/BigQueryClientTest.php index 58cddc7a89fb..b6527511ef23 100644 --- a/tests/snippets/BigQuery/BigQueryClientTest.php +++ b/tests/snippets/BigQuery/BigQueryClientTest.php @@ -63,8 +63,8 @@ class BigQueryClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \BigQueryClientStub; - $this->client->setConnection($this->connection->reveal()); + $this->client = \Google\Cloud\Dev\stub(BigQueryClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testClass() @@ -91,7 +91,7 @@ public function testRunQuery() $this->connection->getQueryResults(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($this->result); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('queryResults'); $this->assertInstanceOf(QueryResults::class, $res->returnVal()); @@ -134,7 +134,7 @@ public function testRunQueryWithNamedParameters() $this->connection->getQueryResults(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($this->result); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('queryResults'); $this->assertInstanceOf(QueryResults::class, $res->returnVal()); @@ -167,7 +167,7 @@ public function testRunQueryWithPositionalParameters() $this->connection->getQueryResults(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($this->result); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('queryResults'); $this->assertInstanceOf(QueryResults::class, $res->returnVal()); @@ -198,7 +198,7 @@ public function testRunQueryAsJob() ], $this->result ); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('queryResults'); $this->assertInstanceOf(QueryResults::class, $res->returnVal()); @@ -209,7 +209,7 @@ public function testJob() { $snippet = $this->snippetFromMethod(BigQueryClient::class, 'job'); $snippet->addLocal('bigQuery', $this->client); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('job'); $this->assertInstanceOf(Job::class, $res->returnVal()); @@ -230,7 +230,7 @@ public function testJobs() ] ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('jobs'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -241,7 +241,7 @@ public function testDataset() { $snippet = $this->snippetFromMethod(BigQueryClient::class, 'dataset'); $snippet->addLocal('bigQuery', $this->client); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('dataset'); $this->assertInstanceOf(Dataset::class, $res->returnVal()); @@ -262,7 +262,7 @@ public function testDatasets() ] ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('datasets'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -276,7 +276,7 @@ public function testCreateDataset() $this->connection->insertDataset(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn([]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('dataset'); $this->assertInstanceOf(Dataset::class, $res->returnVal()); @@ -286,7 +286,7 @@ public function testBytes() { $snippet = $this->snippetFromMethod(BigQueryClient::class, 'bytes'); $snippet->addLocal('bigQuery', $this->client); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('bytes'); $this->assertInstanceOf(Bytes::class, $res->returnVal()); @@ -296,7 +296,7 @@ public function testDate() { $snippet = $this->snippetFromMethod(BigQueryClient::class, 'date'); $snippet->addLocal('bigQuery', $this->client); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('date'); $this->assertInstanceOf(Date::class, $res->returnVal()); @@ -306,7 +306,7 @@ public function testInt64() { $snippet = $this->snippetFromMethod(BigQueryClient::class, 'int64'); $snippet->addLocal('bigQuery', $this->client); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('int64'); $this->assertInstanceOf(Int64::class, $res->returnVal()); @@ -316,7 +316,7 @@ public function testTime() { $snippet = $this->snippetFromMethod(BigQueryClient::class, 'time'); $snippet->addLocal('bigQuery', $this->client); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('time'); $this->assertInstanceOf(Time::class, $res->returnVal()); @@ -326,7 +326,7 @@ public function testTimestamp() { $snippet = $this->snippetFromMethod(BigQueryClient::class, 'timestamp'); $snippet->addLocal('bigQuery', $this->client); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('timestamp'); $this->assertInstanceOf(Timestamp::class, $res->returnVal()); diff --git a/tests/snippets/BigQuery/QueryResultsTest.php b/tests/snippets/BigQuery/QueryResultsTest.php index f79b7450a992..058164489466 100644 --- a/tests/snippets/BigQuery/QueryResultsTest.php +++ b/tests/snippets/BigQuery/QueryResultsTest.php @@ -63,14 +63,14 @@ public function setUp() $this->reload = []; $this->connection = $this->prophesize(ConnectionInterface::class); - $this->qr = new \QueryResultsStub( + $this->qr = \Google\Cloud\Dev\stub(QueryResults::class, [ $this->connection->reveal(), self::JOB_ID, self::PROJECT, $this->info, $this->reload, new ValueMapper(false) - ); + ]); } public function testRows() @@ -82,7 +82,7 @@ public function testRows() $this->connection->getQueryResults(Argument::any()) ->willReturn($this->info); - $this->qr->setConnection($this->connection->reveal()); + $this->qr->___setProperty('connection', $this->connection->reveal()); $this->qr->reload(); @@ -99,7 +99,7 @@ public function testIsComplete() $this->connection->getQueryResults(Argument::any()) ->willReturn($this->info); - $this->qr->setConnection($this->connection->reveal()); + $this->qr->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Query complete!', $res->output()); @@ -129,7 +129,7 @@ public function testReload() ->shouldBeCalled() ->willReturn(['jobComplete' => true] + $this->info); - $this->qr->setConnection($this->connection->reveal()); + $this->qr->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(QueryResults::class, 'reload'); $snippet->addLocal('queryResults', $this->qr); diff --git a/tests/snippets/BigQuery/TableTest.php b/tests/snippets/BigQuery/TableTest.php index c694d24de82d..088b1e3cf2c1 100644 --- a/tests/snippets/BigQuery/TableTest.php +++ b/tests/snippets/BigQuery/TableTest.php @@ -27,6 +27,7 @@ use Google\Cloud\Core\Upload\MultipartUploader; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Storage\Connection\ConnectionInterface as StorageConnectionInterface; +use Google\Cloud\Storage\StorageClient; use Prophecy\Argument; /** @@ -66,14 +67,14 @@ public function setUp() $this->mapper = new ValueMapper(false); $this->connection = $this->prophesize(ConnectionInterface::class); - $this->table = new \TableStub( + $this->table = \Google\Cloud\Dev\Stub(Table::class, [ $this->connection->reveal(), self::ID, self::DSID, self::PROJECT, $this->mapper, $this->info - ); + ]); } public function testExists() @@ -85,7 +86,7 @@ public function testExists() ->shouldBeCalled() ->willReturn([]); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Table exists!', $res->output()); @@ -99,7 +100,7 @@ public function testDelete() $this->connection->deleteTable(Argument::any()) ->shouldBeCalled(); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -112,7 +113,7 @@ public function testUpdate() $this->connection->patchTable(Argument::any()) ->shouldBeCalled(); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -128,7 +129,7 @@ public function testRows() 'rows' => $this->info['rows'] ]); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('rows'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -137,11 +138,11 @@ public function testRows() public function testCopy() { - $bq = new \BigQueryClientStub; + $bq = \Google\Cloud\Dev\stub(BigQueryClient::class); $snippet = $this->snippetFromMethod(Table::class, 'copy'); $snippet->addLocal('bigQuery', $bq); - $bq->setConnection($this->connection->reveal()); + $bq->___setProperty('connection', $this->connection->reveal()); $this->connection->insertJob(Argument::any()) ->shouldBeCalled() @@ -151,7 +152,7 @@ public function testCopy() ] ]); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('job'); $this->assertInstanceOf(Job::class, $res->returnVal()); @@ -159,8 +160,8 @@ public function testCopy() public function testExport() { - $storage = new \StorageClientStub; - $storage->setConnection($this->prophesize(StorageConnectionInterface::class)->reveal()); + $storage = \Google\Cloud\Dev\stub(StorageClient::class); + $storage->___setProperty('connection', $this->prophesize(StorageConnectionInterface::class)->reveal()); $snippet = $this->snippetFromMethod(Table::class, 'export'); $snippet->addLocal('storage', $storage); @@ -174,7 +175,7 @@ public function testExport() ] ]); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('job'); $this->assertInstanceOf(Job::class, $res->returnVal()); @@ -199,7 +200,7 @@ public function testLoad() ->shouldBeCalled() ->willReturn($uploader->reveal()); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('job'); $this->assertInstanceOf(Job::class, $res->returnVal()); @@ -207,8 +208,8 @@ public function testLoad() public function testLoadFromStorage() { - $storage = new \StorageClientStub; - $storage->setConnection($this->prophesize(StorageConnectionInterface::class)->reveal()); + $storage = \Google\Cloud\Dev\stub(StorageClient::class); + $storage->___setProperty('connection', $this->prophesize(StorageConnectionInterface::class)->reveal()); $snippet = $this->snippetFromMethod(Table::class, 'loadFromStorage'); $snippet->addLocal('storage', $storage); @@ -227,7 +228,7 @@ public function testLoadFromStorage() ->shouldBeCalled() ->willReturn($uploader->reveal()); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('job'); $this->assertInstanceOf(Job::class, $res->returnVal()); @@ -244,7 +245,7 @@ public function testInsertRow() ]); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('insertResponse'); $this->assertInstanceOf(InsertResponse::class, $res->returnVal()); @@ -261,7 +262,7 @@ public function testInsertRows() ]); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('insertResponse'); $this->assertInstanceOf(InsertResponse::class, $res->returnVal()); @@ -287,7 +288,7 @@ public function testReload() 'friendlyName' => 'El Jefe' ]); - $this->table->setConnection($this->connection->reveal()); + $this->table->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('El Jefe', $res->output()); diff --git a/tests/snippets/BigQuery/TimeTest.php b/tests/snippets/BigQuery/TimeTest.php index 9f782d703d81..0b3e072da53e 100644 --- a/tests/snippets/BigQuery/TimeTest.php +++ b/tests/snippets/BigQuery/TimeTest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Tests\Snippets\BigQuery; +use Google\Cloud\BigQuery\BigQueryClient; use Google\Cloud\BigQuery\Time; use Google\Cloud\Dev\Snippet\SnippetTestCase; @@ -28,7 +29,7 @@ class TimeTest extends SnippetTestCase public function testClass() { $snippet = $this->snippetFromClass(Time::class); - $snippet->addLocal('bigQuery', new \BigQueryClientStub); + $snippet->addLocal('bigQuery', \Google\Cloud\Dev\stub(BigQueryClient::class)); $res = $snippet->invoke('time'); $this->assertInstanceOf(Time::class, $res->returnVal()); diff --git a/tests/snippets/BigQuery/TimestampTest.php b/tests/snippets/BigQuery/TimestampTest.php index 9d79bfa88023..d732c8ce3cec 100644 --- a/tests/snippets/BigQuery/TimestampTest.php +++ b/tests/snippets/BigQuery/TimestampTest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Tests\Snippets\BigQuery; +use Google\Cloud\BigQuery\BigQueryClient; use Google\Cloud\BigQuery\Timestamp; use Google\Cloud\Dev\Snippet\SnippetTestCase; @@ -28,7 +29,7 @@ class TimestampTest extends SnippetTestCase public function testClass() { $snippet = $this->snippetFromClass(Timestamp::class); - $snippet->addLocal('bigQuery', new \BigQueryClientStub); + $snippet->addLocal('bigQuery', \Google\Cloud\Dev\stub(BigQueryClient::class)); $res = $snippet->invoke('timestamp'); $this->assertInstanceOf(Timestamp::class, $res->returnVal()); diff --git a/tests/snippets/Core/Iam/IamTest.php b/tests/snippets/Core/Iam/IamTest.php index efec040851ed..456e92f4d6e6 100644 --- a/tests/snippets/Core/Iam/IamTest.php +++ b/tests/snippets/Core/Iam/IamTest.php @@ -39,8 +39,8 @@ public function setUp() $this->resource = 'testObject'; $this->connection = $this->prophesize(IamConnectionInterface::class); - $this->iam = new \IamStub($this->connection->reveal(), $this->resource); - $this->iam->setConnection($this->connection->reveal()); + $this->iam = \Google\Cloud\Dev\stub(Iam::class, [$this->connection->reveal(), $this->resource]); + $this->iam->___setProperty('connection', $this->connection->reveal()); } public function testClass() @@ -60,7 +60,7 @@ public function testPolicy() ->shouldBeCalled() ->willReturn('foo'); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('policy'); @@ -92,7 +92,7 @@ public function testSetPolicy() 'resource' => $this->resource ])->shouldBeCalled()->willReturn('foo'); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('policy'); @@ -116,7 +116,7 @@ public function testTestPermissions() ->shouldBeCalled() ->willReturn(['permissions' => $permissions]); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('allowedPermissions'); $this->assertEquals($permissions, $res->returnVal()); @@ -131,7 +131,7 @@ public function testReload() ->shouldBeCalled() ->willReturn('foo'); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('policy'); $this->assertEquals('foo', $res->returnVal()); diff --git a/tests/snippets/Core/LongRunning/LongRunningOperationTest.php b/tests/snippets/Core/LongRunning/LongRunningOperationTest.php new file mode 100644 index 000000000000..7f02270652b9 --- /dev/null +++ b/tests/snippets/Core/LongRunning/LongRunningOperationTest.php @@ -0,0 +1,301 @@ +connection = $this->prophesize(LongRunningConnectionInterface::class); + $this->callables = [ + ['typeUrl' => self::TYPE, 'callable' => function($res) { return $res; }] + ]; + $this->operation = \Google\Cloud\Dev\stub(LongRunningOperation::class, [ + $this->connection->reveal(), + self::NAME, + $this->callables + ]); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'name'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke('name'); + $this->assertEquals(self::NAME, $res->returnVal()); + } + + public function testDone() + { + $this->connection->get(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'done' => true, + 'response' => [], + 'metadata' => [ + 'typeUrl' => self::TYPE + ] + ]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'done'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke(); + $this->assertEquals('The operation is done!', $res->output()); + } + + public function testStateInProgress() + { + $this->connection->get(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'done' => false + ]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'state'); + $snippet->addLocal('operation', $this->operation); + $snippet->addUse(LongRunningOperation::class); + + $res = $snippet->invoke(); + $this->assertEquals('Operation is in progress', $res->output()); + } + + public function testStateDone() + { + $this->connection->get(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'done' => true, + 'response' => [ + 'foo' => 'bar' + ], + 'metadata' => [ + 'typeUrl' => self::TYPE + ] + ]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'state'); + $snippet->addUse(LongRunningOperation::class); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke(); + $this->assertEquals('Operation succeeded', $res->output()); + } + + public function testStateFailed() + { + $this->connection->get(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'done' => true, + 'response' => [], + 'error' => [], + 'metadata' => [ + 'typeUrl' => self::TYPE + ] + ]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'state'); + $snippet->addLocal('operation', $this->operation); + $snippet->addUse(LongRunningOperation::class); + + $res = $snippet->invoke(); + $this->assertEquals('Operation failed', $res->output()); + } + + public function testResult() + { + $result = [ + 'foo' => 'bar' + ]; + $this->connection->get(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'done' => true, + 'response' => $result, + 'metadata' => [ + 'typeUrl' => self::TYPE + ] + ]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'result'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke('result'); + $this->assertEquals($result, $res->returnVal()); + } + + public function testError() + { + $result = [ + 'foo' => 'bar' + ]; + $this->connection->get(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'done' => true, + 'response' => [], + 'error' => $result, + 'metadata' => [ + 'typeUrl' => self::TYPE + ] + ]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'error'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke('error'); + $this->assertEquals($result, $res->returnVal()); + } + + public function testInfo() + { + $result = [ + 'done' => true, + 'response' => [ + 'foo' => 'bar' + ], + 'metadata' => [ + 'typeUrl' => self::TYPE + ] + ]; + $this->connection->get(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($result); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'info'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke('info'); + $this->assertEquals($result, $res->returnVal()); + + $snippet->invoke(); + } + + public function testReload() + { + $result = [ + 'done' => true, + 'response' => [ + 'foo' => 'bar' + ], + 'metadata' => [ + 'typeUrl' => self::TYPE + ] + ]; + $this->connection->get(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn($result); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'reload'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke('result'); + $this->assertEquals($result, $res->returnVal()); + + $snippet->invoke(); + } + + public function testPollUntilComplete() + { + $result1 = [ + 'done' => false, + ]; + + $result2 = [ + 'done' => true, + 'response' => [ + 'foo' => 'bar' + ], + 'metadata' => [ + 'typeUrl' => self::TYPE + ] + ]; + + $this->connection->get(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn($result1, $result2); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'pollUntilComplete'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke('result'); + $this->assertEquals($result2['response'], $res->returnVal()); + } + + public function testCancel() + { + $this->connection->cancel(Argument::any()) + ->shouldBeCalled(); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'cancel'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke(); + } + + public function testDelete() + { + $this->connection->delete(Argument::any()) + ->shouldBeCalled(); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(LongRunningOperation::class, 'delete'); + $snippet->addLocal('operation', $this->operation); + + $res = $snippet->invoke(); + } +} diff --git a/tests/snippets/Datastore/Query/GqlQueryTest.php b/tests/snippets/Datastore/Query/GqlQueryTest.php index 6eac4c542cdd..d8158e42bff9 100644 --- a/tests/snippets/Datastore/Query/GqlQueryTest.php +++ b/tests/snippets/Datastore/Query/GqlQueryTest.php @@ -19,8 +19,9 @@ use Google\Cloud\Datastore\Connection\ConnectionInterface; use Google\Cloud\Datastore\DatastoreClient; -use Google\Cloud\Datastore\EntityMapper; use Google\Cloud\Datastore\EntityIterator; +use Google\Cloud\Datastore\EntityMapper; +use Google\Cloud\Datastore\Operation; use Google\Cloud\Datastore\Query\GqlQuery; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Prophecy\Argument; @@ -38,12 +39,12 @@ public function setUp() { $this->datastore = new DatastoreClient; $this->connection = $this->prophesize(ConnectionInterface::class); - $this->operation = new \OperationStub( + $this->operation = \Google\Cloud\Dev\Stub(Operation::class, [ $this->connection->reveal(), 'my-awesome-project', '', new EntityMapper('my-awesome-project', true, false) - ); + ]); } public function testClass() @@ -69,7 +70,7 @@ public function testClass() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromClass(GqlQuery::class); $snippet->addLocal('operation', $this->operation); diff --git a/tests/snippets/Datastore/Query/QueryTest.php b/tests/snippets/Datastore/Query/QueryTest.php index da561cccf0e8..d7074c6e5079 100644 --- a/tests/snippets/Datastore/Query/QueryTest.php +++ b/tests/snippets/Datastore/Query/QueryTest.php @@ -22,6 +22,7 @@ use Google\Cloud\Datastore\EntityIterator; use Google\Cloud\Datastore\EntityMapper; use Google\Cloud\Datastore\Key; +use Google\Cloud\Datastore\Operation; use Google\Cloud\Datastore\Query\Query; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Prophecy\Argument; @@ -42,12 +43,12 @@ public function setUp() $this->datastore = new DatastoreClient; $this->connection = $this->prophesize(ConnectionInterface::class); - $this->operation = new \OperationStub( + $this->operation = \Google\Cloud\Dev\stub(Operation::class, [ $this->connection->reveal(), 'my-awesome-project', '', $mapper - ); + ]); $this->query = new Query($mapper); } @@ -76,7 +77,7 @@ public function testClass() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromClass(Query::class); $snippet->addLocal('operation', $this->operation); diff --git a/tests/snippets/Datastore/TransactionTest.php b/tests/snippets/Datastore/TransactionTest.php index f15784a8fd6c..a6f701677d79 100644 --- a/tests/snippets/Datastore/TransactionTest.php +++ b/tests/snippets/Datastore/TransactionTest.php @@ -44,12 +44,12 @@ class TransactionTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->operation = new \OperationStub( + $this->operation = \Google\Cloud\Dev\stub(Operation::class, [ $this->connection->reveal(), self::PROJECT, '', new EntityMapper(self::PROJECT, false, false) - ); + ]); $this->transaction = new Transaction($this->operation, self::PROJECT, $this->transactionId); $this->datastore = new DatastoreClient; $this->key = new Key('my-awesome-project', [ @@ -101,7 +101,7 @@ public function testInsert() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -126,7 +126,7 @@ public function testInsertBatch() $this->allocateIdsConnectionMock(); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -152,7 +152,7 @@ public function testUpdate() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -198,7 +198,7 @@ public function testUpsert() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -241,7 +241,7 @@ public function testDelete() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -292,7 +292,7 @@ public function testLookup() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Bob', $res->output()); @@ -342,7 +342,7 @@ public function testLookupBatch() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals("Bob", explode("\n", $res->output())[0]); @@ -382,7 +382,7 @@ public function testRunQuery() ] ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('result'); $this->assertEquals('Bob', $res->output()); @@ -396,7 +396,7 @@ public function testCommit() $this->connection->commit(Argument::any()) ->shouldBeCalled(); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -409,7 +409,7 @@ public function testRollback() $this->connection->rollback(Argument::any()) ->shouldBeCalled(); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } diff --git a/tests/snippets/Language/LanguageClientTest.php b/tests/snippets/Language/LanguageClientTest.php index d23d1811f7a6..f2462709c9b2 100644 --- a/tests/snippets/Language/LanguageClientTest.php +++ b/tests/snippets/Language/LanguageClientTest.php @@ -33,8 +33,8 @@ class LanguageClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \LanguageClientStub; - $this->client->setConnection($this->connection->reveal()); + $this->client = \Google\Cloud\Dev\stub(NaturalLanguageClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testClass() @@ -56,7 +56,7 @@ public function testAnalyzeEntities() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(LanguageClient::class, 'analyzeEntities'); $snippet->addLocal('language', $this->client); @@ -75,7 +75,7 @@ public function testAnalyzeSentiment() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(LanguageClient::class, 'analyzeSentiment'); $snippet->addLocal('language', $this->client); @@ -98,7 +98,7 @@ public function testAnalyzeSyntax() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(LanguageClient::class, 'analyzeSyntax'); $snippet->addLocal('language', $this->client); @@ -117,7 +117,7 @@ public function testAnnotateTextAllFeatures() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(LanguageClient::class, 'annotateText'); $snippet->addLocal('language', $this->client); @@ -139,7 +139,7 @@ public function testAnnotateTextSomeFeatures() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(LanguageClient::class, 'annotateText', 1); $snippet->addLocal('language', $this->client); diff --git a/tests/snippets/Logging/LoggerTest.php b/tests/snippets/Logging/LoggerTest.php index 3fd1f0da55a8..5c74fcd2d3a9 100644 --- a/tests/snippets/Logging/LoggerTest.php +++ b/tests/snippets/Logging/LoggerTest.php @@ -38,11 +38,11 @@ class LoggerTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->logger = new \LoggerStub( + $this->logger = \Google\Cloud\Dev\stub(Logger::class, [ $this->connection->reveal(), self::NAME, self::PROJECT - ); + ]); } public function testClass() @@ -61,7 +61,7 @@ public function testDelete() $this->connection->deleteLog(Argument::any()) ->shouldBeCalled(); - $this->logger->setConnection($this->connection->reveal()); + $this->logger->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -80,7 +80,7 @@ public function testEntries() ] ]); - $this->logger->setConnection($this->connection->reveal()); + $this->logger->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('entries'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -106,7 +106,7 @@ public function testEntriesWithFilter() ] ]); - $this->logger->setConnection($this->connection->reveal()); + $this->logger->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('entries'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -150,7 +150,7 @@ public function testWrite() $this->connection->writeEntries(Argument::any()) ->shouldBeCalled(); - $this->logger->setConnection($this->connection->reveal()); + $this->logger->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -164,7 +164,7 @@ public function testWriteKeyValLevel() $this->connection->writeEntries(Argument::any()) ->shouldBeCalled(); - $this->logger->setConnection($this->connection->reveal()); + $this->logger->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -177,7 +177,7 @@ public function testWriteFactory() $this->connection->writeEntries(Argument::any()) ->shouldBeCalled(); - $this->logger->setConnection($this->connection->reveal()); + $this->logger->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -190,7 +190,7 @@ public function testWriteBatch() $this->connection->writeEntries(Argument::any()) ->shouldBeCalled(); - $this->logger->setConnection($this->connection->reveal()); + $this->logger->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } diff --git a/tests/snippets/Logging/LoggingClientTest.php b/tests/snippets/Logging/LoggingClientTest.php index e794bb8627d5..48cc81a34a7b 100644 --- a/tests/snippets/Logging/LoggingClientTest.php +++ b/tests/snippets/Logging/LoggingClientTest.php @@ -38,8 +38,8 @@ class LoggingClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \LoggingClientStub; - $this->client->setConnection($this->connection->reveal()); + $this->client = \Google\Cloud\Dev\stub(LoggingClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testClass() @@ -86,7 +86,7 @@ public function testSinks() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('sinks'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -103,7 +103,7 @@ public function testCreateMetric() ->shouldBeCalled() ->willReturn([]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('metric'); $this->assertInstanceOf(Metric::class, $res->returnVal()); @@ -132,7 +132,7 @@ public function testMetrics() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('metrics'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -154,7 +154,7 @@ public function testEntries() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('entries'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -179,7 +179,7 @@ public function testEntriesWithFilter() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('entries'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); diff --git a/tests/snippets/Logging/MetricTest.php b/tests/snippets/Logging/MetricTest.php index 747dadaffb38..240cf7c890d2 100644 --- a/tests/snippets/Logging/MetricTest.php +++ b/tests/snippets/Logging/MetricTest.php @@ -36,11 +36,11 @@ class MetricTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->metric = new \MetricStub( + $this->metric = \Google\Cloud\Dev\stub(Metric::class, [ $this->connection->reveal(), self::METRIC, self::PROJECT - ); + ]); } public function testClass() @@ -60,7 +60,7 @@ public function testExists() ->shouldBeCalled() ->willReturn([]); - $this->metric->setConnection($this->connection->reveal()); + $this->metric->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals("Metric exists!", $res->output()); @@ -74,7 +74,7 @@ public function testDelete() $this->connection->deleteMetric(Argument::any()) ->shouldBeCalled(); - $this->metric->setConnection($this->connection->reveal()); + $this->metric->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -92,7 +92,7 @@ public function testUpdate() ->shouldBeCalled() ->willReturn(['description' => 'Foo']); - $this->metric->setConnection($this->connection->reveal()); + $this->metric->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -106,7 +106,7 @@ public function testInfo() ->shouldBeCalled() ->willReturn(['description' => 'Foo']); - $this->metric->setConnection($this->connection->reveal()); + $this->metric->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Foo', $res->output()); @@ -121,7 +121,7 @@ public function testReload() ->shouldBeCalled() ->willReturn(['description' => 'Foo']); - $this->metric->setConnection($this->connection->reveal()); + $this->metric->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Foo', $res->output()); diff --git a/tests/snippets/Logging/SinkTest.php b/tests/snippets/Logging/SinkTest.php index 1a8f9c2cc345..91cd4c77392a 100644 --- a/tests/snippets/Logging/SinkTest.php +++ b/tests/snippets/Logging/SinkTest.php @@ -36,7 +36,11 @@ class SinkTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->sink = new \SinkStub($this->connection->reveal(), self::SINK, self::PROJECT); + $this->sink = \Google\Cloud\Dev\stub(Sink::class, [ + $this->connection->reveal(), + self::SINK, + self::PROJECT + ]); } public function testClass() @@ -56,7 +60,7 @@ public function testExists() ->shouldBeCalled() ->willReturn([]); - $this->sink->setConnection($this->connection->reveal()); + $this->sink->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Sink exists!', $res->output()); @@ -70,7 +74,7 @@ public function testDelete() $this->connection->deleteSink(Argument::any()) ->shouldBeCalled(); - $this->sink->setConnection($this->connection->reveal()); + $this->sink->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -89,7 +93,7 @@ public function testUpdate() 'destination' => 'Foo' ]); - $this->sink->setConnection($this->connection->reveal()); + $this->sink->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -105,7 +109,7 @@ public function testInfo() 'destination' => 'Foo' ]); - $this->sink->setConnection($this->connection->reveal()); + $this->sink->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Foo', $res->output()); @@ -122,7 +126,7 @@ public function testReload() 'destination' => 'Foo' ]); - $this->sink->setConnection($this->connection->reveal()); + $this->sink->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Foo', $res->output()); diff --git a/tests/snippets/PubSub/PubSubClientTest.php b/tests/snippets/PubSub/PubSubClientTest.php index 87c9cc53eb44..4f1e2ab4b904 100644 --- a/tests/snippets/PubSub/PubSubClientTest.php +++ b/tests/snippets/PubSub/PubSubClientTest.php @@ -45,7 +45,7 @@ class PubSubClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \PubSubClientStub(['transport' => 'rest']); + $this->client = \Google\Cloud\Dev\stub(PubSubClient::class, [['transport' => 'rest']]); } public function testClassExample() @@ -73,7 +73,7 @@ public function testCreateTopic() 'name' => self::TOPIC ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(PubSubClient::class, 'createTopic'); $snippet->addLocal('pubsub', $this->client); @@ -96,7 +96,7 @@ public function testTopic() 'name' => self::TOPIC ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('topic'); @@ -118,7 +118,7 @@ public function testTopics() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('topics'); @@ -138,7 +138,7 @@ public function testSubscribe() 'topic' => self::TOPIC ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('subscription'); @@ -160,7 +160,7 @@ public function testSubscription() 'topic' => self::TOPIC ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('subscription'); @@ -183,7 +183,7 @@ public function testSubscriptions() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('subscriptions'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); diff --git a/tests/snippets/PubSub/SubscriptionTest.php b/tests/snippets/PubSub/SubscriptionTest.php index cbc0e95e220b..ddfce689118e 100644 --- a/tests/snippets/PubSub/SubscriptionTest.php +++ b/tests/snippets/PubSub/SubscriptionTest.php @@ -41,16 +41,16 @@ class SubscriptionTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->subscription = new \SubscriptionStub( + $this->subscription = \Google\Cloud\Dev\stub(Subscription::class, [ $this->connection->reveal(), 'foo', self::SUBSCRIPTION, self::TOPIC, false - ); + ]); - $this->pubsub = new \PubSubClientStub(['transport' => 'rest']); - $this->pubsub->setConnection($this->connection->reveal()); + $this->pubsub = \Google\Cloud\Dev\stub(PubSubClient::class, [['transport' => 'rest']]); + $this->pubsub->___setProperty('connection', $this->connection->reveal()); } public function testClassThroughTopic() @@ -89,7 +89,7 @@ public function testCreate() ->shouldBeCalled() ->willReturn($return); - $this->pubsub->setConnection($this->connection->reveal()); + $this->pubsub->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('result'); $this->assertEquals($return, $res->returnVal()); @@ -116,7 +116,7 @@ public function testDelete() $this->connection->deleteSubscription(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -129,7 +129,7 @@ public function testExists() $this->connection->getSubscription(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Subscription exists!', $res->output()); @@ -144,7 +144,7 @@ public function testInfo() ->shouldBeCalled() ->willReturn(['name' => self::SUBSCRIPTION]); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals(self::SUBSCRIPTION, $res->output()); @@ -159,7 +159,7 @@ public function testReload() ->shouldBeCalled() ->willReturn(['name' => self::SUBSCRIPTION]); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); @@ -180,7 +180,7 @@ public function testPull() ] ]); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('messages'); $this->assertContainsOnlyInstancesOf(Message::class, $res->returnVal()); @@ -203,7 +203,7 @@ public function testAcknowledge() $this->connection->acknowledge(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -224,7 +224,7 @@ public function testAcknowledgeBatch() $this->connection->acknowledge(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -248,7 +248,7 @@ public function testModifyAckDeadline() $this->connection->modifyAckDeadline(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -272,7 +272,7 @@ public function testModifyAckDeadlineBatch() $this->connection->modifyAckDeadline(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -285,7 +285,7 @@ public function testModifyPushConfig() $this->connection->modifyPushConfig(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } diff --git a/tests/snippets/PubSub/TopicTest.php b/tests/snippets/PubSub/TopicTest.php index bafb98fa7f34..64e3d3595d7b 100644 --- a/tests/snippets/PubSub/TopicTest.php +++ b/tests/snippets/PubSub/TopicTest.php @@ -17,9 +17,10 @@ namespace Google\Cloud\Tests\Snippets\PubSub; -use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Core\Iam\Iam; +use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\PubSub\Connection\ConnectionInterface; +use Google\Cloud\PubSub\PubSubClient; use Google\Cloud\PubSub\Subscription; use Google\Cloud\PubSub\Topic; use Google\Cloud\Core\Iterator\ItemIterator; @@ -34,23 +35,25 @@ class TopicTest extends SnippetTestCase const SUBSCRIPTION = 'projects/my-awesome-project/subscriptions/my-new-subscription'; private $connection; + private $pubsub; private $topic; public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->topic = new \TopicStub( + $this->pubsub = \Google\Cloud\Dev\stub(PubSubClient::class); + $this->topic = \Google\Cloud\Dev\stub(Topic::class, [ $this->connection->reveal(), 'my-awesome-project', self::TOPIC, false - ); + ]); } public function testClass() { $snippet = $this->snippetFromClass(Topic::class); - $snippet->addLocal('pubsub', new \PubSubClientStub(['transport' => 'rest'])); + $snippet->addLocal('pubsub', $this->pubsub); $res = $snippet->invoke('topic'); $this->assertInstanceOf(Topic::class, $res->returnVal()); @@ -60,7 +63,7 @@ public function testClass() public function testClassWithFullyQualifiedName() { $snippet = $this->snippetFromClass(Topic::class, 1); - $snippet->addLocal('pubsub', new \PubSubClientStub(['transport' => 'rest'])); + $snippet->addLocal('pubsub', $this->pubsub); $res = $snippet->invoke('topic'); $this->assertInstanceOf(Topic::class, $res->returnVal()); @@ -85,7 +88,7 @@ public function testCreate() ->shouldBeCalled() ->willReturn([]); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('topicInfo'); $this->assertEquals([], $res->returnVal()); @@ -99,7 +102,7 @@ public function testDelete() $this->connection->deleteTopic(Argument::any()) ->shouldBeCalled(); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -113,7 +116,7 @@ public function testExists() ->shouldBeCalled() ->willReturn([]); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Topic exists', $res->output()); @@ -130,7 +133,7 @@ public function testInfo() 'name' => self::TOPIC ]); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals(self::TOPIC, $res->output()); @@ -147,7 +150,7 @@ public function testReload() 'name' => self::TOPIC ]); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals(self::TOPIC, $res->output()); @@ -161,7 +164,7 @@ public function testPublish() $this->connection->publishMessage(Argument::any()) ->shouldBeCalled(); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -174,7 +177,7 @@ public function testPublishBatch() $this->connection->publishMessage(Argument::any()) ->shouldBeCalled(); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -190,7 +193,7 @@ public function testSubscribe() 'name' => self::SUBSCRIPTION ]); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('subscription'); $this->assertInstanceOf(Subscription::class, $res->returnVal()); @@ -218,7 +221,7 @@ public function testSubscriptions() ] ]); - $this->topic->setConnection($this->connection->reveal()); + $this->topic->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('subscriptions'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); diff --git a/tests/snippets/ServiceBuilderTest.php b/tests/snippets/ServiceBuilderTest.php index 3beee4f1c67d..85e352bf0e18 100644 --- a/tests/snippets/ServiceBuilderTest.php +++ b/tests/snippets/ServiceBuilderTest.php @@ -24,6 +24,7 @@ use Google\Cloud\Language\LanguageClient; use Google\Cloud\PubSub\PubSubClient; use Google\Cloud\ServiceBuilder; +use Google\Cloud\Spanner\SpannerClient; use Google\Cloud\Speech\SpeechClient; use Google\Cloud\Storage\StorageClient; use Google\Cloud\Trace\TraceClient; @@ -56,6 +57,7 @@ public function serviceBuilderMethods() ['logging', LoggingClient::class, 'logging'], ['language', LanguageClient::class, 'language'], ['pubsub', PubSubClient::class, 'pubsub'], + ['spanner', SpannerClient::class, 'spanner'], ['speech', SpeechClient::class, 'speech'], ['storage', StorageClient::class, 'storage'], ['trace', TraceClient::class, 'trace'], diff --git a/tests/snippets/Spanner/BytesTest.php b/tests/snippets/Spanner/BytesTest.php new file mode 100644 index 000000000000..5b1a629247a6 --- /dev/null +++ b/tests/snippets/Spanner/BytesTest.php @@ -0,0 +1,81 @@ +bytes = new Bytes(self::BYTES); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Bytes::class); + $res = $snippet->invoke('bytes'); + $this->assertInstanceOf(Bytes::class, $res->returnVal()); + } + + public function testClassCast() + { + $snippet = $this->snippetFromClass(Bytes::class, 1); + $snippet->addLocal('bytes', $this->bytes); + $res = $snippet->invoke(); + + $this->assertEquals(base64_encode(self::BYTES), $res->output()); + } + + public function testGet() + { + $snippet = $this->snippetFromMethod(Bytes::class, 'get'); + $snippet->addLocal('bytes', $this->bytes); + $res = $snippet->invoke('stream'); + + $this->assertInstanceOf(StreamInterface::class, $res->returnVal()); + $this->assertEquals(self::BYTES, (string)$res->returnVal()); + } + + public function testType() + { + $snippet = $this->snippetFromMethod(Bytes::class, 'type'); + $snippet->addLocal('bytes', $this->bytes); + $res = $snippet->invoke(); + $this->assertEquals(ValueMapper::TYPE_BYTES, $res->output()); + } + + public function testFormatAsString() + { + $snippet = $this->snippetFromMethod(Bytes::class, 'formatAsString'); + $snippet->addLocal('bytes', $this->bytes); + $res = $snippet->invoke(); + + $this->assertEquals(base64_encode(self::BYTES), $res->output()); + } +} diff --git a/tests/snippets/Spanner/ConfigurationTest.php b/tests/snippets/Spanner/ConfigurationTest.php new file mode 100644 index 000000000000..181b5fecebdf --- /dev/null +++ b/tests/snippets/Spanner/ConfigurationTest.php @@ -0,0 +1,122 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->config = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), + self::PROJECT, + self::CONFIG + ]); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Configuration::class); + $res = $snippet->invoke('configuration'); + + $this->assertInstanceOf(Configuration::class, $res->returnVal()); + $this->assertEquals(self::CONFIG, $res->returnVal()->name()); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'name'); + $snippet->addLocal('configuration', $this->config); + + $res = $snippet->invoke('name'); + $this->assertEquals(self::CONFIG, $res->returnVal()); + } + + public function testInfo() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'info'); + $snippet->addLocal('configuration', $this->config); + + $info = [ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]; + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn($info); + + $this->config->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('info'); + $this->assertEquals($info, $res->returnVal()); + } + + public function testExists() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'exists'); + $snippet->addLocal('configuration', $this->config); + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]); + + $this->config->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Configuration exists!', $res->output()); + } + + public function testReload() + { + $info = [ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]; + + $snippet = $this->snippetFromMethod(Configuration::class, 'reload'); + $snippet->addLocal('configuration', $this->config); + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn($info); + + $this->config->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('info'); + $this->assertEquals($info, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/DatabaseTest.php b/tests/snippets/Spanner/DatabaseTest.php new file mode 100644 index 000000000000..e7eb9b50b20b --- /dev/null +++ b/tests/snippets/Spanner/DatabaseTest.php @@ -0,0 +1,713 @@ +prophesize(Instance::class); + $instance->name()->willReturn(self::INSTANCE); + + $session = $this->prophesize(Session::class); + + $sessionPool = $this->prophesize(SessionPoolInterface::class); + $sessionPool->session(Argument::any(), Argument::any(), Argument::any()) + ->willReturn($session->reveal()); + + $this->connection = $this->prophesize(ConnectionInterface::class); + $this->database = \Google\Cloud\Dev\stub(Database::class, [ + $this->connection->reveal(), + $instance->reveal(), + $sessionPool->reveal(), + $this->prophesize(LongRunningConnectionInterface::class)->reveal(), + [], + self::PROJECT, + self::DATABASE + ], ['connection', 'operation']); + } + + private function stubOperation() + { + $operation = \Google\Cloud\Dev\stub(Operation::class, [ + $this->connection->reveal(), false + ]); + + $this->database->___setProperty('operation', $operation); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Database::class); + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + } + + public function testClassViaInstance() + { + $snippet = $this->snippetFromClass(Database::class, 1); + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Database::class, 'name'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke('name'); + $this->assertEquals(self::DATABASE, $res->returnVal()); + } + + /** + * @group spanneradmin + */ + public function testExists() + { + $snippet = $this->snippetFromMethod(Database::class, 'exists'); + $snippet->addLocal('database', $this->database); + + $this->connection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willReturn(['statements' => []]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Database exists!', $res->output()); + } + + /** + * @group spanneradmin + */ + public function testUpdateDdl() + { + $snippet = $this->snippetFromMethod(Database::class, 'updateDdl'); + $snippet->addLocal('database', $this->database); + + $this->connection->updateDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $snippet->invoke(); + } + + /** + * @group spanneradmin + */ + public function testUpdateDdlBatch() + { + $snippet = $this->snippetFromMethod(Database::class, 'updateDdlBatch'); + $snippet->addLocal('database', $this->database); + + $this->connection->updateDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $snippet->invoke(); + } + + /** + * @group spanneradmin + */ + public function testDrop() + { + $snippet = $this->snippetFromMethod(Database::class, 'drop'); + $snippet->addLocal('database', $this->database); + + $this->connection->dropDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $snippet->invoke(); + } + + /** + * @group spanneradmin + */ + public function testDdl() + { + $snippet = $this->snippetFromMethod(Database::class, 'ddl'); + $snippet->addLocal('database', $this->database); + + $stmts = [ + 'CREATE TABLE TestSuites', + 'CREATE TABLE TestCases' + ]; + + $this->connection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'statements' => $stmts + ]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('statements'); + $this->assertEquals($stmts, $res->returnVal()); + } + + public function testSnapshot() + { + $this->connection->beginTransaction(Argument::any(), Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'snapshot'); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke('snapshot'); + $this->assertInstanceOf(Snapshot::class, $res->returnVal()); + } + + public function testSnapshotReadTimestamp() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'id' => self::TRANSACTION, + 'readTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'snapshot', 1); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke('timestamp'); + $this->assertInstanceOf(Timestamp::class, $res->returnVal()); + } + + public function testRunTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->connection->commit(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->connection->rollback(Argument::any()) + ->shouldNotBeCalled(); + + $this->connection->executeSql(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'runTransaction'); + $snippet->addUse(Transaction::class); + $snippet->addLocal('database', $this->database); + $snippet->addLocal('username', 'foo'); + $snippet->addLocal('password', 'bar'); + + $snippet->invoke(); + } + + public function testRunTransactionRollback() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->connection->commit(Argument::any()) + ->shouldNotBeCalled(); + + $this->connection->rollback(Argument::any()) + ->shouldBeCalled(); + + $this->connection->executeSql(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [] + ], + 'rows' => [] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'runTransaction'); + $snippet->addUse(Transaction::class); + $snippet->addLocal('database', $this->database); + $snippet->addLocal('username', 'foo'); + $snippet->addLocal('password', 'bar'); + + $snippet->invoke(); + } + + public function testTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'transaction'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke('transaction'); + $this->assertInstanceOf(Transaction::class, $res->returnVal()); + } + + public function testInsert() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['insert'])) return false; + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'insert'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke(); + } + + + public function testInsertBatch() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['insert'])) return false; + if (!isset($args['mutations'][1]['insert'])) return false; + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'insertBatch'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke(); + } + + public function testUpdate() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['update'])) return false; + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'update'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke(); + } + + + public function testUpdateBatch() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['update'])) return false; + if (!isset($args['mutations'][1]['update'])) return false; + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'updateBatch'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke(); + } + + public function testInsertOrUpdate() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['insertOrUpdate'])) return false; + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'insertOrUpdate'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke(); + } + + + public function testInsertOrUpdateBatch() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['insertOrUpdate'])) return false; + if (!isset($args['mutations'][1]['insertOrUpdate'])) return false; + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'insertOrUpdateBatch'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke(); + } + + public function testReplace() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['replace'])) return false; + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'replace'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke(); + } + + + public function testReplaceBatch() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['replace'])) return false; + if (!isset($args['mutations'][1]['replace'])) return false; + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'replaceBatch'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke(); + } + + public function testDelete() + { + $this->connection->commit(Argument::that(function ($args) { + if (!isset($args['mutations'][0]['delete'])) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'delete'); + $snippet->addUse(KeySet::class); + $snippet->addLocal('database', $this->database); + $snippet->invoke(); + } + + public function testExecute() + { + $this->connection->executeSql(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'execute'); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke('result'); + $this->assertInstanceOf(Result::class, $res->returnVal()); + } + + public function testExecuteBeginSnapshot() + { + $this->connection->executeSql(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ], + 'transaction' => [ + 'id' => self::TRANSACTION + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'execute', 1); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke('result'); + $this->assertInstanceOf(Result::class, $res->returnVal()); + $this->assertInstanceOf(Snapshot::class, $res->returnVal()->snapshot()); + } + + public function testExecuteBeginTransaction() + { + $this->connection->executeSql(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ], + 'transaction' => [ + 'id' => self::TRANSACTION + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'execute', 2); + $snippet->addLocal('database', $this->database); + $snippet->addUse(SessionPoolInterface::class); + + $res = $snippet->invoke('result'); + $this->assertInstanceOf(Result::class, $res->returnVal()); + $this->assertInstanceOf(Transaction::class, $res->returnVal()->transaction()); + } + + public function testRead() + { + $this->connection->read(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'read'); + $snippet->addLocal('database', $this->database); + $snippet->addUse(KeySet::class); + + $res = $snippet->invoke('result'); + $this->assertInstanceOf(Result::class, $res->returnVal()); + } + + public function testReadWithSnapshot() + { + $this->connection->read(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ], + 'transaction' => [ + 'id' => self::TRANSACTION + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'read', 1); + $snippet->addLocal('database', $this->database); + $snippet->addUse(KeySet::class); + + $res = $snippet->invoke('result'); + $this->assertInstanceOf(Result::class, $res->returnVal()); + $this->assertInstanceOf(Snapshot::class, $res->returnVal()->snapshot()); + } + + public function testReadWithTransaction() + { + $this->connection->read(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ], + 'transaction' => [ + 'id' => self::TRANSACTION + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'read', 2); + $snippet->addLocal('database', $this->database); + $snippet->addUse(KeySet::class); + $snippet->addUse(SessionPoolInterface::class); + + $res = $snippet->invoke('result'); + $this->assertInstanceOf(Result::class, $res->returnVal()); + $this->assertInstanceOf(Transaction::class, $res->returnVal()->transaction()); + } + + public function testIam() + { + $snippet = $this->snippetFromMethod(Database::class, 'iam'); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke('iam'); + $this->assertInstanceOf(Iam::class, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/DateTest.php b/tests/snippets/Spanner/DateTest.php new file mode 100644 index 000000000000..96998c73fa27 --- /dev/null +++ b/tests/snippets/Spanner/DateTest.php @@ -0,0 +1,78 @@ +dt = new \DateTimeImmutable; + $this->date = new Date($this->dt); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Date::class); + $res = $snippet->invoke('date'); + + $this->assertInstanceOf(Date::class, $res->returnVal()); + } + + public function testClassString() + { + $snippet = $this->snippetFromClass(Date::class, 1); + $snippet->addLocal('date', $this->date); + $res = $snippet->invoke(); + + $this->assertEquals($this->date->formatAsString(), $res->output()); + } + + public function testGet() + { + $snippet = $this->snippetFromMethod(Date::class, 'get'); + $snippet->addLocal('date', $this->date); + $res = $snippet->invoke('dateTime'); + $this->assertEquals($this->dt, $res->returnVal()); + } + + public function testType() + { + $snippet = $this->snippetFromMethod(Date::class, 'type'); + $snippet->addLocal('date', $this->date); + $res = $snippet->invoke(); + $this->assertEquals(ValueMapper::TYPE_DATE, $res->output()); + } + + public function testFormatAsString() + { + $snippet = $this->snippetFromMethod(Date::class, 'formatAsString'); + $snippet->addLocal('date', $this->date); + $res = $snippet->invoke(); + $this->assertEquals($this->date->formatAsString(), $res->output()); + } +} diff --git a/tests/snippets/Spanner/DurationTest.php b/tests/snippets/Spanner/DurationTest.php new file mode 100644 index 000000000000..228c65463153 --- /dev/null +++ b/tests/snippets/Spanner/DurationTest.php @@ -0,0 +1,80 @@ +duration = new Duration(self::SECONDS, self::NANOS); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Duration::class); + $res = $snippet->invoke('duration'); + $this->assertInstanceOf(Duration::class, $res->returnVal()); + } + + public function testClassCast() + { + $snippet = $this->snippetFromClass(Duration::class, 1); + $snippet->addLocal('duration', $this->duration); + + $res = $snippet->invoke(); + $this->assertEquals($this->duration->formatAsString(), $res->output()); + } + + public function testGet() + { + $snippet = $this->snippetFromMethod(Duration::class, 'get'); + $snippet->addLocal('duration', $this->duration); + + $res = $snippet->invoke('res'); + $this->assertEquals($this->duration->get(), $res->returnVal()); + } + + public function testType() + { + $snippet = $this->snippetFromMethod(Duration::class, 'type'); + $snippet->addLocal('duration', $this->duration); + + $res = $snippet->invoke(); + $this->assertEquals(Duration::TYPE, $res->output()); + } + + public function testFormatAsString() + { + $snippet = $this->snippetFromMethod(Duration::class, 'formatAsString'); + $snippet->addLocal('duration', $this->duration); + + $res = $snippet->invoke(); + $this->assertEquals($this->duration->formatAsString(), $res->output()); + } +} diff --git a/tests/snippets/Spanner/InstanceTest.php b/tests/snippets/Spanner/InstanceTest.php new file mode 100644 index 000000000000..b5bb836df759 --- /dev/null +++ b/tests/snippets/Spanner/InstanceTest.php @@ -0,0 +1,222 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->instance = \Google\Cloud\Dev\stub(Instance::class, [ + $this->connection->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), + $this->prophesize(LongRunningConnectionInterface::class)->reveal(), + [], + self::PROJECT, + self::INSTANCE + ]); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Instance::class); + $res = $snippet->invoke('instance'); + $this->assertInstanceOf(Instance::class, $res->returnVal()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Instance::class, 'name'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('name'); + $this->assertEquals(self::INSTANCE, $res->returnVal()); + } + + public function testInfo() + { + $snippet = $this->snippetFromMethod(Instance::class, 'info'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn(['nodeCount' => 1]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('1', $res->output()); + } + + public function testExists() + { + $snippet = $this->snippetFromMethod(Instance::class, 'exists'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn(['foo' => 'bar']); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Instance exists!', $res->output()); + } + + public function testReload() + { + $snippet = $this->snippetFromMethod(Instance::class, 'reload'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn(['nodeCount' => 1]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('info'); + $info = $this->instance->info(); + $this->assertEquals($info, $res->returnVal()); + } + + public function testState() + { + $snippet = $this->snippetFromMethod(Instance::class, 'state'); + $snippet->addLocal('instance', $this->instance); + $snippet->addUse(Instance::class); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn(['state' => Instance::STATE_READY]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Instance is ready!', $res->output()); + } + + public function testUpdate() + { + $snippet = $this->snippetFromMethod(Instance::class, 'update'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn([ + 'displayName' => 'foo', + 'nodeCount' => 1 + ]); + + $this->connection->updateInstance(Argument::any()) + ->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + $snippet->invoke(); + } + + public function testDelete() + { + $snippet = $this->snippetFromMethod(Instance::class, 'delete'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->deleteInstance(Argument::any()) + ->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + $snippet->invoke(); + } + + public function testCreateDatabase() + { + $snippet = $this->snippetFromMethod(Instance::class, 'createDatabase'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->createDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('database'); + $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); + } + + public function testDatabase() + { + $snippet = $this->snippetFromMethod(Instance::class, 'database'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + } + + public function testDatabases() + { + $snippet = $this->snippetFromMethod(Instance::class, 'databases'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->listDatabases(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'databases' => [ + [ + 'name' => 'projects/'. self::PROJECT .'/instances/'. self::INSTANCE .'/databases/'. self::DATABASE + ] + ] + ]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('databases'); + + $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); + $this->assertInstanceOf(Database::class, $res->returnVal()->current()); + } + + public function testIam() + { + $snippet = $this->snippetFromMethod(Instance::class, 'iam'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('iam'); + $this->assertInstanceOf(Iam::class, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/KeyRangeTest.php b/tests/snippets/Spanner/KeyRangeTest.php new file mode 100644 index 000000000000..869d3694a6bb --- /dev/null +++ b/tests/snippets/Spanner/KeyRangeTest.php @@ -0,0 +1,95 @@ +range = new KeyRange; + } + + public function testClass() + { + $snippet = $this->snippetFromClass(KeyRange::class); + $snippet->addUse(KeyRange::class); + $res = $snippet->invoke('range'); + $this->assertInstanceOf(KeyRange::class, $res->returnVal()); + } + + public function testStart() + { + $this->range->setStart(KeyRange::TYPE_OPEN, ['Bob']); + + $snippet = $this->snippetFromMethod(KeyRange::class, 'start'); + $snippet->addLocal('range', $this->range); + $res = $snippet->invoke('start'); + $this->assertEquals(['Bob'], $res->returnVal()); + } + + public function testSetStart() + { + $snippet = $this->snippetFromMethod(KeyRange::class, 'setStart'); + $snippet->addLocal('range', $this->range); + $snippet->addUse(KeyRange::class); + $res = $snippet->invoke(); + $this->assertEquals(['Bob'], $this->range->start()); + } + + public function testEnd() + { + $this->range->setEnd(KeyRange::TYPE_CLOSED, ['Jill']); + + $snippet = $this->snippetFromMethod(KeyRange::class, 'end'); + $snippet->addLocal('range', $this->range); + $res = $snippet->invoke('end'); + $this->assertEquals(['Jill'], $res->returnVal()); + } + + public function testSetEnd() + { + $snippet = $this->snippetFromMethod(KeyRange::class, 'setEnd'); + $snippet->addLocal('range', $this->range); + $snippet->addUse(KeyRange::class); + $res = $snippet->invoke(); + $this->assertEquals(['Jill'], $this->range->end()); + } + + public function testTypes() + { + $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']); + $this->range->setEnd(KeyRange::TYPE_OPEN, ['foo']); + + $snippet = $this->snippetFromMethod(KeyRange::class, 'types'); + $snippet->addLocal('range', $this->range); + + $res = $snippet->invoke('types'); + $this->assertEquals([ + 'start' => 'startOpen', + 'end' => 'endOpen' + ], $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/KeySetTest.php b/tests/snippets/Spanner/KeySetTest.php new file mode 100644 index 000000000000..c7a2904c53f6 --- /dev/null +++ b/tests/snippets/Spanner/KeySetTest.php @@ -0,0 +1,134 @@ +keyset = new KeySet(); + $this->range = new KeyRange(); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(KeySet::class); + $res = $snippet->invoke('keySet'); + $this->assertInstanceOf(KeySet::class, $res->returnVal()); + } + + public function testRanges() + { + $this->keyset->addRange($this->range); + + $snippet = $this->snippetFromMethod(KeySet::class, 'ranges'); + $snippet->addLocal('keySet', $this->keyset); + + $res = $snippet->invoke('ranges'); + $this->assertEquals($this->range, $res->returnVal()[0]); + } + + public function testAddRange() + { + $snippet = $this->snippetFromMethod(KeySet::class, 'addRange'); + $snippet->addLocal('keySet', $this->keyset); + $snippet->addUse(KeyRange::class); + + $this->assertEmpty($this->keyset->ranges()); + $res = $snippet->invoke(); + $this->assertContainsOnly(KeyRange::class, $this->keyset->ranges()); + } + + public function testSetRanges() + { + $snippet = $this->snippetFromMethod(KeySet::class, 'setRanges'); + $snippet->addLocal('keySet', $this->keyset); + $snippet->addUse(KeyRange::class); + + $this->assertEmpty($this->keyset->ranges()); + $res = $snippet->invoke(); + $this->assertContainsOnly(KeyRange::class, $this->keyset->ranges()); + } + + public function testKeys() + { + $snippet = $this->snippetFromMethod(KeySet::class, 'keys'); + $snippet->addLocal('keySet', $this->keyset); + + $this->keyset->addKey('foo'); + + $res = $snippet->invoke('keys'); + $this->assertEquals('foo', $res->returnVal()[0]); + } + + public function testAddKey() + { + $snippet = $this->snippetFromMethod(KeySet::class, 'addKey'); + $snippet->addLocal('keySet', $this->keyset); + + $this->assertEmpty($this->keyset->keys()); + + $res = $snippet->invoke(); + $this->assertEquals(1, count($this->keyset->keys())); + } + + public function testSetKeys() + { + $snippet = $this->snippetFromMethod(KeySet::class, 'setKeys'); + $snippet->addLocal('keySet', $this->keyset); + + $this->assertEmpty($this->keyset->keys()); + + $res = $snippet->invoke(); + $this->assertEquals(2, count($this->keyset->keys())); + } + + public function testMatchAll() + { + $snippet = $this->snippetFromMethod(KeySet::class, 'matchAll'); + $snippet->addLocal('keySet', $this->keyset); + + $this->assertEmpty($snippet->invoke()->output()); + + $this->keyset->setMatchAll(true); + + $this->assertEquals('All keys will match', $snippet->invoke()->output()); + } + + public function testSetMatchAll() + { + $snippet = $this->snippetFromMethod(KeySet::class, 'setMatchAll'); + $snippet->addLocal('keySet', $this->keyset); + + $this->assertFalse($this->keyset->matchAll()); + + $snippet->invoke(); + + $this->assertTrue($this->keyset->matchAll()); + } +} diff --git a/tests/snippets/Spanner/SnapshotTest.php b/tests/snippets/Spanner/SnapshotTest.php new file mode 100644 index 000000000000..86d7ed889725 --- /dev/null +++ b/tests/snippets/Spanner/SnapshotTest.php @@ -0,0 +1,167 @@ +connection = $this->prophesize(ConnectionInterface::class); + $operation = $this->prophesize(Operation::class); + $session = $this->prophesize(Session::class); + + $this->snapshot = \Google\Cloud\Dev\stub(Snapshot::class, [ + $operation->reveal(), + $session->reveal(), + self::TRANSACTION, + new Timestamp(new \DateTime) + ], ['operation']); + } + + private function stubOperation($stub = null) + { + $operation = \Google\Cloud\Dev\stub(Operation::class, [ + $this->connection->reveal(), false + ]); + + if (!$stub) { + $stub = $this->snapshot; + } + + $stub->___setProperty('operation', $operation); + } + + public function testClass() + { + $database = $this->prophesize(Database::class); + $database->snapshot()->shouldBeCalled()->willReturn('foo'); + + $snippet = $this->snippetFromClass(Snapshot::class); + $snippet->replace('$database =', '//$database ='); + $snippet->addLocal('database', $database->reveal()); + + $res = $snippet->invoke('snapshot'); + $this->assertEquals('foo', $res->returnVal()); + } + + public function testExecute() + { + $this->connection->executeSql(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMagicMethod(Snapshot::class, 'execute'); + $snippet->addLocal('snapshot', $this->snapshot); + $res = $snippet->invoke('result'); + + $this->assertInstanceOf(Result::class, $res->returnVal()); + } + + public function testRead() + { + $this->connection->read(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMagicMethod(Snapshot::class, 'read'); + $snippet->addLocal('snapshot', $this->snapshot); + $snippet->addUse(KeySet::class); + $res = $snippet->invoke('result'); + + $this->assertInstanceOf(Result::class, $res->returnVal()); + } + + public function testId() + { + $snippet = $this->snippetFromMagicMethod(Snapshot::class, 'id'); + $snippet->addLocal('snapshot', $this->snapshot); + + $res = $snippet->invoke('id'); + $this->assertEquals(self::TRANSACTION, $res->returnVal()); + } + + public function testReadTimestamp() + { + $snippet = $this->snippetFromMethod(Snapshot::class, 'readTimestamp'); + $snippet->addLocal('snapshot', $this->snapshot); + + $res = $snippet->invoke('timestamp'); + $this->assertInstanceOf(Timestamp::class, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/SpannerClientTest.php b/tests/snippets/Spanner/SpannerClientTest.php new file mode 100644 index 000000000000..b715cf0f262f --- /dev/null +++ b/tests/snippets/Spanner/SpannerClientTest.php @@ -0,0 +1,273 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->client = \Google\Cloud\Dev\stub(SpannerClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(SpannerClient::class); + $res = $snippet->invoke('spanner'); + $this->assertInstanceOf(SpannerClient::class, $res->returnVal()); + } + + /** + * @group spanneradmin + */ + public function testConfigurations() + { + $this->connection->listConfigs(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instanceConfigs' => [ + ['name' => 'projects/my-awesome-projects/instanceConfigs/Foo'], + ['name' => 'projects/my-awesome-projects/instanceConfigs/Bar'], + ] + ]); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(SpannerClient::class, 'configurations'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('configurations'); + + $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); + $this->assertInstanceOf(Configuration::class, $res->returnVal()->current()); + $this->assertEquals('Foo', $res->returnVal()->current()->name()); + } + + /** + * @group spanneradmin + */ + public function testConfiguration() + { + $configName = 'foo'; + + $snippet = $this->snippetFromMethod(SpannerClient::class, 'configuration'); + $snippet->addLocal('spanner', $this->client); + $snippet->addLocal('configurationName', self::CONFIG); + + $res = $snippet->invoke('configuration'); + $this->assertInstanceOf(Configuration::class, $res->returnVal()); + $this->assertEquals(self::CONFIG, $res->returnVal()->name()); + } + + /** + * @group spanneradmin + */ + public function testCreateInstance() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'createInstance'); + $snippet->addLocal('spanner', $this->client); + $snippet->addLocal('configuration', $this->client->configuration(self::CONFIG)); + + $this->connection->createInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn(['name' => 'operations/foo']); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('operation'); + $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); + } + + /** + * @group spanneradmin + */ + public function testInstance() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'instance'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('instance'); + $this->assertInstanceOf(Instance::class, $res->returnVal()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + } + + /** + * @group spanneradmin + */ + public function testInstances() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'instances'); + $snippet->addLocal('spanner', $this->client); + + $this->connection->listInstances(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instances' => [ + ['name' => 'projects/my-awesome-project/instances/'. self::INSTANCE], + ['name' => 'projects/my-awesome-project/instances/Bar'] + ] + ]); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('instances'); + $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); + $this->assertInstanceOf(Instance::class, $res->returnVal()->current()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->current()->name()); + } + + public function testConnect() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'connect'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + } + + public function testKeySet() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'keySet'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('keySet'); + $this->assertInstanceOf(KeySet::class, $res->returnVal()); + } + + public function testKeySetAll() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'keySet', 1); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('keySet'); + $this->assertInstanceOf(KeySet::class, $res->returnVal()); + $this->assertTrue($res->returnVal()->matchAll()); + } + + public function testKeyRange() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'keyRange'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('range'); + $this->assertInstanceOf(KeyRange::class, $res->returnVal()); + } + + public function testKeyRangeComplete() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'keyRange', 1); + $snippet->addLocal('spanner', $this->client); + $snippet->addUse(KeyRange::class); + + $res = $snippet->invoke('range'); + $this->assertInstanceOf(KeyRange::class, $res->returnVal()); + $res->returnVal()->keyRangeObject(); + } + + public function testBytes() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'bytes'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('bytes'); + $this->assertInstanceOf(Bytes::class, $res->returnVal()); + } + + public function testDate() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'date'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('date'); + $this->assertInstanceOf(Date::class, $res->returnVal()); + } + + public function testTimestamp() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'timestamp'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('timestamp'); + $this->assertInstanceOf(Timestamp::class, $res->returnVal()); + } + + public function testInt64() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'int64'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('int64'); + $this->assertInstanceOf(Int64::class, $res->returnVal()); + } + + public function testDuration() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'duration'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('duration'); + $this->assertInstanceOf(Duration::class, $res->returnVal()); + } + + public function testSessionClient() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'sessionClient'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('sessionClient'); + $this->assertInstanceOf(SessionClient::class, $res->returnVal()); + } + + public function testResumeOperation() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'resumeOperation'); + $snippet->addLocal('spanner', $this->client); + $snippet->addLocal('operationName', 'operations/foo'); + + $res = $snippet->invoke('operation'); + $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/TimestampTest.php b/tests/snippets/Spanner/TimestampTest.php new file mode 100644 index 000000000000..a96512fe0d11 --- /dev/null +++ b/tests/snippets/Spanner/TimestampTest.php @@ -0,0 +1,79 @@ +dt = new \DateTime; + $this->timestamp = new Timestamp($this->dt); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Timestamp::class); + $res = $snippet->invoke('timestamp'); + $this->assertInstanceOf(Timestamp::class, $res->returnVal()); + } + + public function testClassCast() + { + $snippet = $this->snippetFromClass(Timestamp::class, 1); + $snippet->addLocal('timestamp', $this->timestamp); + + $res = $snippet->invoke(); + $this->assertEquals((string)$this->timestamp, $res->output()); + } + + public function testGet() + { + $snippet = $this->snippetFromMethod(Timestamp::class, 'get'); + $snippet->addLocal('timestamp', $this->timestamp); + + $res = $snippet->invoke('dateTime'); + $this->assertEquals($this->dt, $res->returnVal()); + } + + public function testType() + { + $snippet = $this->snippetFromMethod(Timestamp::class, 'type'); + $snippet->addLocal('timestamp', $this->timestamp); + + $res = $snippet->invoke('type'); + $this->assertEquals(ValueMapper::TYPE_TIMESTAMP, $res->returnVal()); + } + + public function testFormatAsString() + { + $snippet = $this->snippetFromMethod(Timestamp::class, 'formatAsString'); + $snippet->addLocal('timestamp', $this->timestamp); + + $res = $snippet->invoke('value'); + $this->assertEquals($this->timestamp->formatAsString(), $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/TransactionTest.php b/tests/snippets/Spanner/TransactionTest.php new file mode 100644 index 000000000000..f0733e22545d --- /dev/null +++ b/tests/snippets/Spanner/TransactionTest.php @@ -0,0 +1,330 @@ +connection = $this->prophesize(ConnectionInterface::class); + $operation = $this->prophesize(Operation::class); + $session = $this->prophesize(Session::class); + + $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, [ + $operation->reveal(), + $session->reveal(), + self::TRANSACTION + ], ['operation']); + } + + private function stubOperation($stub = null) + { + $operation = \Google\Cloud\Dev\stub(Operation::class, [ + $this->connection->reveal(), false + ]); + + if (!$stub) { + $stub = $this->transaction; + } + + $stub->___setProperty('operation', $operation); + } + + public function testClass() + { + $database = $this->prophesize(Database::class); + $database->runTransaction(Argument::type('callable'))->shouldBeCalled(); + + $snippet = $this->snippetFromClass(Transaction::class); + $snippet->replace('$database =', '//$database ='); + $snippet->addLocal('database', $database->reveal()); + + $res = $snippet->invoke(); + } + + public function testClassReturnTransaction() + { + $database = $this->prophesize(Database::class); + $database->transaction() + ->shouldBeCalled() + ->willReturn('foo'); + + $snippet = $this->snippetFromClass(Transaction::class, 1); + $snippet->addLocal('database', $database->reveal()); + $res = $snippet->invoke('transaction'); + $this->assertEquals('foo', $res->returnVal()); + } + + public function testExecute() + { + $this->connection->executeSql(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMagicMethod(Transaction::class, 'execute'); + $snippet->addLocal('transaction', $this->transaction); + $res = $snippet->invoke('result'); + + $this->assertInstanceOf(Result::class, $res->returnVal()); + } + + public function testRead() + { + $this->connection->read(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + 0 + ] + ] + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMagicMethod(Transaction::class, 'read'); + $snippet->addLocal('transaction', $this->transaction); + $snippet->addUse(KeySet::class); + $res = $snippet->invoke('result'); + + $this->assertInstanceOf(Result::class, $res->returnVal()); + } + + public function testId() + { + $snippet = $this->snippetFromMagicMethod(Transaction::class, 'id'); + $snippet->addLocal('transaction', $this->transaction); + + $res = $snippet->invoke('id'); + $this->assertEquals(self::TRANSACTION, $res->returnVal()); + } + + public function testInsert() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'insert'); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['insert'])); + } + + + public function testInsertBatch() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'insertBatch'); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['insert'])); + } + + public function testUpdate() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'update'); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['update'])); + } + + + public function testUpdateBatch() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'updateBatch'); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['update'])); + } + + public function testInsertOrUpdate() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'insertOrUpdate'); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['insertOrUpdate'])); + } + + + public function testInsertOrUpdateBatch() + { + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Transaction::class, 'insertOrUpdateBatch'); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['insertOrUpdate'])); + } + + public function testReplace() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'replace'); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['replace'])); + } + + + public function testReplaceBatch() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'replaceBatch'); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['replace'])); + } + + public function testDelete() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'delete'); + $snippet->addUse(KeySet::class); + $snippet->addLocal('transaction', $this->transaction); + + $this->stubOperation(); + + $res = $snippet->invoke(); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertTrue(isset($mutations[0]['delete'])); + } + + public function testRollback() + { + $this->connection->rollback(Argument::any()) + ->shouldBeCalled(); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Transaction::class, 'rollback'); + $snippet->addLocal('transaction', $this->transaction); + + $snippet->invoke(); + } + + public function testCommit() + { + $this->connection->commit(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() + ]); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Transaction::class, 'commit'); + $snippet->addLocal('transaction', $this->transaction); + + $snippet->invoke(); + } + + public function testState() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'state'); + $snippet->addLocal('transaction', $this->transaction); + + $res = $snippet->invoke('state'); + $this->assertEquals(Transaction::STATE_ACTIVE, $res->returnVal()); + } +} diff --git a/tests/snippets/Speech/OperationTest.php b/tests/snippets/Speech/OperationTest.php index 006c823c05e0..e6f06e449e05 100644 --- a/tests/snippets/Speech/OperationTest.php +++ b/tests/snippets/Speech/OperationTest.php @@ -48,7 +48,11 @@ public function setUp() ]; $this->connection = $this->prophesize(ConnectionInterface::class); - $this->operation = new \SpeechOperationStub($this->connection->reveal(), $this->opData['name'], $this->opData); + $this->operation = \Google\Cloud\Dev\stub(Operation::class, [ + $this->connection->reveal(), + $this->opData['name'], + $this->opData + ]); } public function testClass() @@ -118,7 +122,7 @@ public function testReload() ->shouldBeCalled() ->willReturn($this->opData); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals(print_r($this->opData['response'], true), $res->output()); diff --git a/tests/snippets/Speech/SpeechClientTest.php b/tests/snippets/Speech/SpeechClientTest.php index b6c167b4a6e8..03c6fd70c636 100644 --- a/tests/snippets/Speech/SpeechClientTest.php +++ b/tests/snippets/Speech/SpeechClientTest.php @@ -36,8 +36,8 @@ public function setUp() { $this->testFile = "'" . __DIR__ . '/../fixtures/Speech/demo.flac' . "'"; $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \SpeechClientStub(['languageCode' => 'en-US']); - $this->client->setConnection($this->connection->reveal()); + $this->client = \Google\Cloud\Dev\stub(SpeechClient::class, ['languageCode' => 'en-US']); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testClass() @@ -69,7 +69,7 @@ public function testRecognize() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals($transcript . PHP_EOL, $res->output()); @@ -96,7 +96,7 @@ public function testRecognizeWithOptions() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals($transcript . PHP_EOL, $res->output()); @@ -127,7 +127,7 @@ public function testBeginRecognizeOperation() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals(print_r($results[0]['alternatives'][0], true), $res->output()); @@ -158,7 +158,7 @@ public function testBeginRecognizeOperationWithOptions() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals(print_r($results[0]['alternatives'][0], true), $res->output()); diff --git a/tests/snippets/Storage/AclTest.php b/tests/snippets/Storage/AclTest.php index a81f9ea4f0c8..0a34b035a685 100644 --- a/tests/snippets/Storage/AclTest.php +++ b/tests/snippets/Storage/AclTest.php @@ -33,7 +33,11 @@ class AclTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->acl = new \AclStub($this->connection->reveal(), 'bucketAccessControls', []); + $this->acl = \Google\Cloud\Dev\stub(Acl::class, [ + $this->connection->reveal(), + 'bucketAccessControls', + [] + ]); } public function testClass() { @@ -51,7 +55,7 @@ public function testDelete() $this->connection->deleteAcl(Argument::any()) ->shouldBeCalled(); - $this->acl->setConnection($this->connection->reveal()); + $this->acl->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -65,7 +69,7 @@ public function testGet() ->shouldBeCalled() ->willReturn('foo'); - $this->acl->setConnection($this->connection->reveal()); + $this->acl->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('res'); $this->assertEquals('foo', $res->returnVal()); @@ -79,7 +83,7 @@ public function testAdd() $this->connection->insertAcl(Argument::any()) ->shouldBecalled(); - $this->acl->setConnection($this->connection->reveal()); + $this->acl->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -92,7 +96,7 @@ public function testUpdate() $this->connection->patchAcl(Argument::any()) ->shouldBeCalled(); - $this->acl->setConnection($this->connection->reveal()); + $this->acl->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } } diff --git a/tests/snippets/Storage/BucketTest.php b/tests/snippets/Storage/BucketTest.php index 5340699587ae..18fe1d01319d 100644 --- a/tests/snippets/Storage/BucketTest.php +++ b/tests/snippets/Storage/BucketTest.php @@ -43,7 +43,10 @@ class BucketTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->bucket = new \BucketStub($this->connection->reveal(), self::BUCKET); + $this->bucket = \Google\Cloud\Dev\stub(Bucket::class, [ + $this->connection->reveal(), + self::BUCKET + ]); } public function testClass() @@ -82,7 +85,7 @@ public function testExists() $this->connection->getBucket(Argument::any()) ->shouldBeCalled(); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Bucket exists!', $res->output()); @@ -106,7 +109,7 @@ public function testUpload() ->shouldBeCalled() ->willReturn($uploader->reveal()); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('object'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -133,7 +136,7 @@ public function testUploadResumableUploader() ->shouldBeCalled() ->willReturn($uploader->reveal()); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('object'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -160,7 +163,7 @@ public function testUploadEncryption() ->shouldBeCalled() ->willReturn($uploader->reveal()); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('object'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -194,7 +197,7 @@ public function testGetResumableUploader() ->shouldBeCalled() ->willReturn($uploader->reveal()); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('object'); } @@ -214,7 +217,7 @@ public function testGetStreamableUploader() ->shouldBeCalled() ->willReturn($uploader->reveal()); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -248,7 +251,7 @@ public function testObjects() ] ]); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('objects'); $this->assertInstanceOf(ObjectIterator::class, $res->returnVal()); @@ -264,7 +267,7 @@ public function testDelete() $this->connection->deleteBucket(Argument::any()) ->shouldBeCalled(); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -283,7 +286,7 @@ public function testUpdate() ->shouldBeCalled() ->willReturn('foo'); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); } @@ -300,7 +303,7 @@ public function testCompose() 'generation' => 'foo' ]); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('singleObject'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -319,7 +322,7 @@ public function testComposeWithObjects() 'generation' => 'foo' ]); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('singleObject'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -338,7 +341,7 @@ public function testInfo() 'location' => $loc ]); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals($loc, $res->output()); @@ -356,7 +359,7 @@ public function testReload() 'location' => $loc ]); - $this->bucket->setConnection($this->connection->reveal()); + $this->bucket->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals($loc, $res->output()); diff --git a/tests/snippets/Storage/StorageClientTest.php b/tests/snippets/Storage/StorageClientTest.php index 906f2cd4aba6..0f5d98583a69 100644 --- a/tests/snippets/Storage/StorageClientTest.php +++ b/tests/snippets/Storage/StorageClientTest.php @@ -37,8 +37,8 @@ class StorageClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \StorageClientStub; - $this->client->setConnection($this->connection->reveal()); + $this->client = \Google\Cloud\Dev\stub(StorageClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testClass() @@ -71,7 +71,7 @@ public function testBuckets() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('buckets'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -95,7 +95,7 @@ public function testBucketsWithPrefix() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('buckets'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -112,7 +112,7 @@ public function testCreateBucket() ->shouldBeCalled() ->willReturn([]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('bucket'); $this->assertInstanceOf(Bucket::class, $res->returnVal()); @@ -127,9 +127,9 @@ public function testCreateBucketWithLogging() ->shouldBeCalled() ->willReturn([]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('bucket'); $this->assertInstanceOf(Bucket::class, $res->returnVal()); } -} + } diff --git a/tests/snippets/Storage/StorageObjectTest.php b/tests/snippets/Storage/StorageObjectTest.php index 0cd7f6e5fdd6..475ee81e9baa 100644 --- a/tests/snippets/Storage/StorageObjectTest.php +++ b/tests/snippets/Storage/StorageObjectTest.php @@ -40,11 +40,11 @@ class StorageObjectTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->object = new \StorageObjectStub( + $this->object = \Google\Cloud\Dev\stub(StorageObject::class, [ $this->connection->reveal(), self::OBJECT, self::BUCKET - ); + ]); } public function testClass() @@ -72,7 +72,7 @@ public function testExists() ->shouldBeCalled() ->willReturn([]); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Object exists!', $res->output()); @@ -86,7 +86,7 @@ public function testDelete() $this->connection->deleteObject(Argument::any()) ->shouldBeCalled(); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -99,7 +99,7 @@ public function testUpdate() $this->connection->patchObject(Argument::any()) ->shouldBeCalled(); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -117,7 +117,7 @@ public function testCopy() 'generation' => 'foo' ]); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('copiedObject'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -144,7 +144,7 @@ public function testCopyToBucket() 'generation' => 'foo' ]); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('copiedObject'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -165,7 +165,7 @@ public function testRewrite() ] ]); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('rewrittenObject'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -194,7 +194,7 @@ public function testRewriteNewObjectName() ] ]); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('rewrittenObject'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -216,7 +216,7 @@ public function testRewriteNewKey() ] ]); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('rewrittenObject'); $this->assertInstanceOf(StorageObject::class, $res->returnVal()); @@ -238,7 +238,7 @@ public function testRename() $this->connection->deleteObject(Argument::any()) ->shouldBeCalled(); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('object2.txt', $res->output()); @@ -253,7 +253,7 @@ public function testDownloadAsString() ->shouldBeCalled() ->willReturn(\GuzzleHttp\Psr7\stream_for('test')); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('test', $res->output()); @@ -269,7 +269,7 @@ public function testDownloadToFile() ->shouldBeCalled() ->willReturn(\GuzzleHttp\Psr7\stream_for('test')); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('stream'); @@ -285,7 +285,7 @@ public function testDownloadAsStream() ->shouldBeCalled() ->willReturn(\GuzzleHttp\Psr7\stream_for('test')); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); @@ -306,7 +306,7 @@ public function testInfo() 'location' => 'right behind you!' ]); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('1', $res->output()); @@ -326,7 +326,7 @@ public function testReload() 'location' => 'right behind you!' ]); - $this->object->setConnection($this->connection->reveal()); + $this->object->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('right behind you!', $res->output()); diff --git a/tests/snippets/Translate/TranslateClientTest.php b/tests/snippets/Translate/TranslateClientTest.php index 7f748e1ac0b2..00ba0252d86f 100644 --- a/tests/snippets/Translate/TranslateClientTest.php +++ b/tests/snippets/Translate/TranslateClientTest.php @@ -33,8 +33,8 @@ class TranslateClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \TranslateClientStub; - $this->client->setConnection($this->connection->reveal()); + $this->client = \Google\Cloud\Dev\stub(TranslateClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testClass() @@ -63,7 +63,7 @@ public function testTranslate() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('foobar', $res->output()); @@ -87,7 +87,7 @@ public function testTranslateBatch() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('foobar', $res->output()); @@ -113,7 +113,7 @@ public function testDetectLanguage() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('en', $res->output()); @@ -139,7 +139,7 @@ public function testDetectLanguageBatch() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('en', $res->output()); @@ -163,7 +163,7 @@ public function testLanguages() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('en', $res->output()); @@ -187,7 +187,7 @@ public function testLocalizedLanguages() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('en', $res->output()); diff --git a/tests/snippets/Vision/VisionClientTest.php b/tests/snippets/Vision/VisionClientTest.php index 832c33591c4b..e24f00ee0b9f 100644 --- a/tests/snippets/Vision/VisionClientTest.php +++ b/tests/snippets/Vision/VisionClientTest.php @@ -35,8 +35,8 @@ class VisionClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \VisionClientStub; - $this->client->setConnection($this->connection->reveal()); + $this->client = \Google\Cloud\Dev\stub(VisionClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testClassWithServiceBuilder() @@ -115,7 +115,7 @@ public function testAnnotate() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('result'); @@ -145,7 +145,7 @@ public function testAnnotateBatch() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('result'); diff --git a/tests/snippets/bootstrap.php b/tests/snippets/bootstrap.php index 8fbd4cc1ed1c..298292891bbb 100644 --- a/tests/snippets/bootstrap.php +++ b/tests/snippets/bootstrap.php @@ -33,35 +33,3 @@ exit(1); } }); - -function stub($name, $extends) -{ - $tpl = 'class %s extends %s {use \Google\Cloud\Dev\SetStubConnectionTrait; }'; - - eval(sprintf($tpl, $name, $extends)); -} - -stub('AclStub', Google\Cloud\Storage\Acl::class); -stub('BigQueryClientStub', Google\Cloud\BigQuery\BigQueryClient::class); -stub('BucketStub', Google\Cloud\Storage\Bucket::class); -stub('DatastoreClientStub', Google\Cloud\Datastore\DatastoreClient::class); -stub('IamStub', Google\Cloud\Core\Iam\Iam::class); -stub('LoggerStub', Google\Cloud\Logging\Logger::class); -stub('LoggingClientStub', Google\Cloud\Logging\LoggingClient::class); -stub('MetricStub', Google\Cloud\Logging\Metric::class); -stub('LanguageClientStub', Google\Cloud\Language\LanguageClient::class); -stub('OperationStub', Google\Cloud\Datastore\Operation::class); -stub('PubSubClientStub', Google\Cloud\PubSub\PubSubClient::class); -stub('QueryResultsStub', Google\Cloud\BigQuery\QueryResults::class); -stub('SinkStub', Google\Cloud\Logging\Sink::class); -stub('SnapshotStub', Google\Cloud\PubSub\Snapshot::class); -stub('SpeechClientStub', Google\Cloud\Speech\SpeechClient::class); -stub('SpeechOperationStub', Google\Cloud\Speech\Operation::class); -stub('StorageClientStub', Google\Cloud\Storage\StorageClient::class); -stub('StorageObjectStub', Google\Cloud\Storage\StorageObject::class); -stub('SubscriptionStub', Google\Cloud\PubSub\Subscription::class); -stub('TableStub', Google\Cloud\BigQuery\Table::class); -stub('TopicStub', Google\Cloud\PubSub\Topic::class); -stub('TraceClientStub', Google\Cloud\Trace\TraceClient::class); -stub('TranslateClientStub', Google\Cloud\Translate\TranslateClient::class); -stub('VisionClientStub', Google\Cloud\Vision\VisionClient::class); diff --git a/tests/system/Spanner/AdminTest.php b/tests/system/Spanner/AdminTest.php new file mode 100644 index 000000000000..545deb89989a --- /dev/null +++ b/tests/system/Spanner/AdminTest.php @@ -0,0 +1,102 @@ +instances(); + $instance = array_filter(iterator_to_array($instances), function ($instance) { + return $instance->name() === self::INSTANCE_NAME; + }); + + $this->assertInstanceOf(Instance::class, $instance[0]); + + $instance = self::$instance; + $this->assertTrue($instance->exists()); + $this->assertEquals($instance->name(), $this->parseName($instance->info()['name'])); + $this->assertEquals($instance->name(), $this->parseName($instance->reload()['name'])); + + $this->assertEquals(Instance::STATE_READY, $instance->state()); + + $displayName = uniqid(self::TESTING_PREFIX); + $op = $instance->update([ + 'displayName' => $displayName + ]); + + $this->assertInstanceOf(LongRunningOperation::class, $op); + $op->pollUntilComplete(); + + $instance = $client->instance(self::INSTANCE_NAME); + $this->assertEquals($displayName, $instance->info()['displayName']); + } + + public function testDatabase() + { + $instance = self::$instance; + + $dbName = uniqid(self::TESTING_PREFIX); + $op = $instance->createDatabase($dbName); + + $this->assertInstanceOf(LongRunningOperation::class, $op); + $db = $op->pollUntilComplete(); + $this->assertInstanceOf(Database::class, $db); + + self::$deletionQueue[] = function() use ($db) { $db->drop(); }; + + $databases = $instance->databases(); + $database = array_filter(iterator_to_array($databases), function ($db) use ($dbName) { + return $db->name() === $dbName; + }); + + $this->assertInstanceOf(Database::class, current($database)); + + $this->assertTrue($db->exists()); + + $stmt = "CREATE TABLE Ids (\n" . + " id INT64 NOT NULL,\n" . + ") PRIMARY KEY(id)"; + + $op = $db->updateDdl($stmt); + $op->pollUntilComplete(); + + $this->assertEquals($db->ddl()[0], $stmt); + } + + private function parseName($name) + { + return InstanceAdminClient::parseInstanceFromInstanceName($name); + } + + private function parseDbName($name) + { + return DatabaseAdminClient::parseDatabaseFromDatabaseName($name); + } +} diff --git a/tests/system/Spanner/ConfigurationTest.php b/tests/system/Spanner/ConfigurationTest.php new file mode 100644 index 000000000000..94ded41522f1 --- /dev/null +++ b/tests/system/Spanner/ConfigurationTest.php @@ -0,0 +1,53 @@ +configurations(); + + $this->assertContainsOnly(Configuration::class, $configurations); + + $res = iterator_to_array($configurations); + $firstConfigName = $res[0]->name(); + + $config = $client->configuration($firstConfigName); + + $this->assertInstanceOf(Configuration::class, $config); + $this->assertEquals($firstConfigName, $config->name()); + + $this->assertTrue($config->exists()); + $this->assertEquals($config->name(), $this->parseName($config->info()['name'])); + $this->assertEquals($config->name(), $this->parseName($config->reload()['name'])); + } + + private function parseName($name) + { + return InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($name); + } +} diff --git a/tests/system/Spanner/OperationsTest.php b/tests/system/Spanner/OperationsTest.php new file mode 100644 index 000000000000..fcfb952858f2 --- /dev/null +++ b/tests/system/Spanner/OperationsTest.php @@ -0,0 +1,142 @@ +insert(self::TEST_TABLE_NAME, [ + 'id' => $this->id, + 'name' => 'Bob', + 'birthday' => self::$client->date(new \DateTime('2000-01-01')) + ]); + + $this->assertInstanceOf(Timestamp::class, $res); + } + + /** + * @depends testInsert + */ + public function testExecute() + { + $db = self::$database; + + $row = $this->getRow(); + $this->assertEquals($this->id, $row['id']); + } + + /** + * @depends testInsert + */ + public function testRead() + { + $db = self::$database; + + $keySet = self::$client->keySet([ + 'keys' => [$this->id] + ]); + $columns = ['id', 'name']; + + $res = $db->read(self::TEST_TABLE_NAME, $keySet, $columns); + $row = $res->firstRow(); + $this->assertEquals($this->id, $row['id']); + } + + /** + * @depends testInsert + */ + public function testUpdate() + { + $db = self::$database; + $row = $this->getRow(); + $row['name'] = 'Doug'; + + $db->update('Users', $row); + + $row = $this->getRow(); + $this->assertEquals('Doug', $row['name']); + } + + /** + * @depends testInsert + */ + public function testInsertOrUpdate() + { + $db = self::$database; + $db->insertOrUpdate('Users', [ + 'id' => $this->id, + 'name' => 'Dave', + 'birthday' => new Date(new \DateTime('1990-01-01')) + ]); + + $row = $this->getRow(); + $this->assertEquals('Dave', $row['name']); + } + + /** + * @depends testInsert + */ + public function testReplace() + { + $db = self::$database; + $db->replace('Users', [ + 'id' => $this->id, + 'name' => 'John', + 'birthday' => new Date(new \DateTime('1990-01-01')) + ]); + + $row = $this->getRow(); + $this->assertEquals('John', $row['name']); + } + + /** + * @depends testInsert + */ + public function testDelete() + { + $db = self::$database; + $keySet = self::$client->keySet([ + 'keys' => [$this->id] + ]); + + $db->delete(self::TEST_TABLE_NAME, $keySet); + $this->assertNull($this->getRow()); + } + + private function getRow() + { + $db = self::$database; + $res = $db->execute('SELECT * FROM '. self::TEST_TABLE_NAME .' WHERE id=@id', [ + 'parameters' => [ + 'id' => $this->id + ] + ]); + + return $res->firstRow(); + } +} diff --git a/tests/system/Spanner/SnapshotTest.php b/tests/system/Spanner/SnapshotTest.php new file mode 100644 index 000000000000..d6f6e78cf784 --- /dev/null +++ b/tests/system/Spanner/SnapshotTest.php @@ -0,0 +1,57 @@ +id = rand(1,99999); + } + + public function testSnapshot() + { + $db = self::$database; + + $db->insert('Users', [ + 'id' => $this->id, + 'name' => 'John', + 'birthday' => new Date(new \DateTime('1990-01-01')) + ]); + + $snapshot = $db->snapshot(); + $row = $this->getRow($snapshot); + $this->assertEquals('John', $row['name']); + } + + private function getRow($client) + { + return $client->execute('SELECT * FROM Users WHERE id=@id', [ + 'parameters' => [ + 'id' => $this->id + ] + ])->firstRow(); + } +} diff --git a/tests/system/Spanner/SpannerTestCase.php b/tests/system/Spanner/SpannerTestCase.php new file mode 100644 index 000000000000..264a5542a6dd --- /dev/null +++ b/tests/system/Spanner/SpannerTestCase.php @@ -0,0 +1,80 @@ + $keyFilePath + ]); + + self::$instance = self::$client->instance(self::INSTANCE_NAME); + + $dbName = uniqid(self::TESTING_PREFIX); + $op = self::$instance->createDatabase($dbName); + $op->pollUntilComplete(); + $db = self::$client->connect(self::INSTANCE_NAME, $dbName); + + self::$deletionQueue[] = function() use ($db) { $db->drop(); }; + + $op = $db->updateDdl( + 'CREATE TABLE '. self::TEST_TABLE_NAME .' ( + id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + birthday DATE NOT NULL + ) PRIMARY KEY (id)' + ); + $op->pollUntilComplete(); + + self::$database = $db; + } + + public static function tearDownFixtures() + { + $backoff = new ExponentialBackoff(8); + + foreach (self::$deletionQueue as $item) { + $backoff->execute($item); + } + } +} diff --git a/tests/system/Spanner/TransactionTest.php b/tests/system/Spanner/TransactionTest.php new file mode 100644 index 000000000000..1ce4f5825a81 --- /dev/null +++ b/tests/system/Spanner/TransactionTest.php @@ -0,0 +1,46 @@ +runTransaction(function ($t) { + $id = rand(1,346464); + $t->insert(self::TEST_TABLE_NAME, [ + 'id' => $id, + 'name' => uniqid(self::TESTING_PREFIX), + 'birthday' => new Date(new \DateTime) + ]); + + $t->commit(); + }); + + $db->runTransaction(function ($t) { + $t->rollback(); + }); + } +} diff --git a/tests/system/bootstrap.php b/tests/system/bootstrap.php index ca21ed09e3e6..f9e2b36a997c 100644 --- a/tests/system/bootstrap.php +++ b/tests/system/bootstrap.php @@ -2,11 +2,12 @@ require __DIR__ . '/../../vendor/autoload.php'; -use Google\Cloud\Tests\System\PubSub\PubSubTestCase; +use Google\Cloud\Tests\System\BigQuery\BigQueryTestCase; use Google\Cloud\Tests\System\Datastore\DatastoreTestCase; -use Google\Cloud\Tests\System\Storage\StorageTestCase; use Google\Cloud\Tests\System\Logging\LoggingTestCase; -use Google\Cloud\Tests\System\BigQuery\BigQueryTestCase; +use Google\Cloud\Tests\System\PubSub\PubSubTestCase; +use Google\Cloud\Tests\System\Spanner\SpannerTestCase; +use Google\Cloud\Tests\System\Storage\StorageTestCase; if (!getenv('GOOGLE_CLOUD_PHP_TESTS_KEY_PATH')) { throw new \Exception( @@ -20,4 +21,5 @@ StorageTestCase::tearDownFixtures(); LoggingTestCase::tearDownFixtures(); BigQueryTestCase::tearDownFixtures(); + SpannerTestCase::tearDownFixtures(); }); diff --git a/tests/unit/Core/ArrayTraitTest.php b/tests/unit/Core/ArrayTraitTest.php index 362b6e124923..2577051fd200 100644 --- a/tests/unit/Core/ArrayTraitTest.php +++ b/tests/unit/Core/ArrayTraitTest.php @@ -83,6 +83,26 @@ public function testIsAssocFalse() $this->assertFalse($actual); } + + public function testArrayFilterRemoveNull() + { + $input = [ + 'null' => null, + 'false' => false, + 'zero' => 0, + 'float' => 0.0, + 'empty' => '', + 'array' => [], + ]; + + $res = $this->implementation->call('arrayFilterRemoveNull', [$input]); + $this->assertFalse(array_key_exists('null', $res)); + $this->assertTrue(array_key_exists('false', $res)); + $this->assertTrue(array_key_exists('zero', $res)); + $this->assertTrue(array_key_exists('float', $res)); + $this->assertTrue(array_key_exists('empty', $res)); + $this->assertTrue(array_key_exists('array', $res)); + } } class ArrayTraitStub diff --git a/tests/unit/Core/PhpArrayTest.php b/tests/unit/Core/PhpArrayTest.php index 7a207cc2d605..1bb285b5190e 100644 --- a/tests/unit/Core/PhpArrayTest.php +++ b/tests/unit/Core/PhpArrayTest.php @@ -28,7 +28,7 @@ class PhpArrayTest extends \PHPUnit_Framework_TestCase { private function getCodec($customFilters = []) { - return new PhpArray($customFilters); + return new PhpArray(['customFilters' => $customFilters]); } /** diff --git a/tests/unit/Datastore/OperationTest.php b/tests/unit/Datastore/OperationTest.php index fee62dd72d4c..0df35468774d 100644 --- a/tests/unit/Datastore/OperationTest.php +++ b/tests/unit/Datastore/OperationTest.php @@ -893,6 +893,11 @@ public function testInvalidBatchType() class OperationStub extends Operation { + // public function runQuery(QueryInterface $q, array $args = []) + // { + // echo 'test'; + // exit; + // } public function setConnection($connection) { $this->connection = $connection; diff --git a/tests/unit/Spanner/BytesTest.php b/tests/unit/Spanner/BytesTest.php new file mode 100644 index 000000000000..d224738f96e2 --- /dev/null +++ b/tests/unit/Spanner/BytesTest.php @@ -0,0 +1,52 @@ +content); + $this->assertEquals($this->content, $bytes->get()); + } + + public function testFormatAsString() + { + $bytes = new Bytes($this->content); + $this->assertEquals(base64_encode($this->content), $bytes->formatAsString()); + } + + public function testCast() + { + $bytes = new Bytes($this->content); + $this->assertEquals(base64_encode($this->content), (string) $bytes); + } + + public function testType() + { + $bytes = new Bytes($this->content); + $this->assertTrue(is_integer($bytes->type())); + } +} diff --git a/tests/unit/Spanner/ConfigurationTest.php b/tests/unit/Spanner/ConfigurationTest.php new file mode 100644 index 000000000000..9cdceb8c336d --- /dev/null +++ b/tests/unit/Spanner/ConfigurationTest.php @@ -0,0 +1,116 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->configuration = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), + self::PROJECT_ID, + self::NAME + ]); + } + + public function testName() + { + $this->assertEquals(self::NAME, $this->configuration->name()); + } + + public function testInfo() + { + $this->connection->getConfig(Argument::any())->shouldNotBeCalled(); + $this->configuration->___setProperty('connection', $this->connection->reveal()); + + $info = ['foo' => 'bar']; + $config = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), + self::PROJECT_ID, + self::NAME, + $info + ]); + + $this->assertEquals($info, $config->info()); + } + + public function testInfoWithReload() + { + $info = ['foo' => 'bar']; + + $this->connection->getConfig([ + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), + 'projectId' => self::PROJECT_ID + ])->shouldBeCalled()->willReturn($info); + + $this->configuration->___setProperty('connection', $this->connection->reveal()); + + $this->assertEquals($info, $this->configuration->info()); + } + + public function testExists() + { + $this->connection->getConfig(Argument::any())->willReturn([]); + $this->configuration->___setProperty('connection', $this->connection->reveal()); + + $this->assertTrue($this->configuration->exists()); + } + + public function testExistsDoesntExist() + { + $this->connection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404)); + $this->configuration->___setProperty('connection', $this->connection->reveal()); + + $this->assertFalse($this->configuration->exists()); + } + + public function testReload() + { + $info = ['foo' => 'bar']; + + $this->connection->getConfig([ + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), + 'projectId' => self::PROJECT_ID + ])->shouldBeCalledTimes(1)->willReturn($info); + + $this->configuration->___setProperty('connection', $this->connection->reveal()); + + $info = $this->configuration->reload(); + + $info2 = $this->configuration->info(); + + $this->assertEquals($info, $info2); + } +} diff --git a/tests/unit/Spanner/Connection/IamDatabaseTest.php b/tests/unit/Spanner/Connection/IamDatabaseTest.php new file mode 100644 index 000000000000..fafbb2f93eaf --- /dev/null +++ b/tests/unit/Spanner/Connection/IamDatabaseTest.php @@ -0,0 +1,66 @@ +connection = $this->prophesize(ConnectionInterface::class); + + $this->iam = \Google\Cloud\Dev\stub(IamDatabase::class, [$this->connection->reveal()]); + } + + /** + * @dataProvider methodProvider + */ + public function testMethods($methodName, $proxyName, $args) + { + $this->connection->$proxyName($args) + ->shouldBeCalled() + ->willReturn($args); + + $this->iam->___setProperty('connection', $this->connection->reveal()); + + $res = $this->iam->$methodName($args); + $this->assertEquals($args, $res); + } + + public function methodProvider() + { + $args = ['foo' => 'bar']; + + return [ + ['getPolicy', 'getDatabaseIamPolicy', $args], + ['setPolicy', 'setDatabaseIamPolicy', $args], + ['testPermissions', 'testDatabaseIamPermissions', $args] + ]; + } +} diff --git a/tests/unit/Spanner/Connection/IamInstanceTest.php b/tests/unit/Spanner/Connection/IamInstanceTest.php new file mode 100644 index 000000000000..426a4a125245 --- /dev/null +++ b/tests/unit/Spanner/Connection/IamInstanceTest.php @@ -0,0 +1,66 @@ +connection = $this->prophesize(ConnectionInterface::class); + + $this->iam = \Google\Cloud\Dev\stub(IamInstance::class, [$this->connection->reveal()]); + } + + /** + * @dataProvider methodProvider + */ + public function testMethods($methodName, $proxyName, $args) + { + $this->connection->$proxyName($args) + ->shouldBeCalled() + ->willReturn($args); + + $this->iam->___setProperty('connection', $this->connection->reveal()); + + $res = $this->iam->$methodName($args); + $this->assertEquals($args, $res); + } + + public function methodProvider() + { + $args = ['foo' => 'bar']; + + return [ + ['getPolicy', 'getInstanceIamPolicy', $args], + ['setPolicy', 'setInstanceIamPolicy', $args], + ['testPermissions', 'testInstanceIamPermissions', $args] + ]; + } +} diff --git a/tests/unit/Spanner/Connection/LongRunningConnectionTest.php b/tests/unit/Spanner/Connection/LongRunningConnectionTest.php new file mode 100644 index 000000000000..f1ce2489d281 --- /dev/null +++ b/tests/unit/Spanner/Connection/LongRunningConnectionTest.php @@ -0,0 +1,66 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->lro = \Google\Cloud\Dev\stub(LongRunningConnection::class, [ + $this->connection->reveal() + ]); + } + + /** + * @dataProvider methodProvider + */ + public function testMethods($methodName, $proxyName, $args) + { + $this->connection->$proxyName($args) + ->shouldBeCalled() + ->willReturn($args); + + $this->lro->___setProperty('connection', $this->connection->reveal()); + + $res = $this->lro->$methodName($args); + $this->assertEquals($args, $res); + } + + public function methodProvider() + { + $args = ['foo' => 'bar']; + + return [ + ['get', 'getOperation', $args], + ['cancel', 'cancelOperation', $args], + ['delete', 'deleteOperation', $args], + ['operations', 'listOperations', $args] + ]; + } +} diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php new file mode 100644 index 000000000000..5216a69fc6f0 --- /dev/null +++ b/tests/unit/Spanner/DatabaseTest.php @@ -0,0 +1,620 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->instance = $this->prophesize(Instance::class); + $this->sessionPool = $this->prophesize(SessionPoolInterface::class); + $this->lro = $this->prophesize(LongRunningConnectionInterface::class); + $this->lroCallables = []; + + $this->sessionPool->session(self::INSTANCE, self::DATABASE, Argument::any()) + ->willReturn(new Session( + $this->connection->reveal(), + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + )); + + $this->instance->name()->willReturn(self::INSTANCE); + + $args = [ + $this->connection->reveal(), + $this->instance->reveal(), + $this->sessionPool->reveal(), + $this->lro->reveal(), + $this->lroCallables, + self::PROJECT, + self::DATABASE, + ]; + + $props = [ + 'connection', 'operation' + ]; + + $this->database = \Google\Cloud\Dev\stub(Database::class, $args, $props); + } + + /** + * @group spanneradmin + */ + public function testExists() + { + $this->connection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willReturn([]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->assertTrue($this->database->exists()); + } + + /** + * @group spanneradmin + */ + public function testExistsNotFound() + { + $this->connection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willThrow(new NotFoundException('', 404)); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->assertFalse($this->database->exists()); + } + + /** + * @group spanneradmin + */ + public function testUpdateDdl() + { + $statement = 'foo'; + $this->connection->updateDatabase([ + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE), + 'statements' => [$statement] + ]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->database->updateDdl($statement); + } + + /** + * @group spanneradmin + */ + public function testUpdateDdlBatch() + { + $statements = ['foo', 'bar']; + $this->connection->updateDatabase([ + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE), + 'statements' => $statements + ]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->database->updateDdl($statements); + } + + /** + * @group spanneradmin + */ + public function testUpdateWithSingleStatement() + { + $statement = 'foo'; + $this->connection->updateDatabase([ + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE), + 'statements' => ['foo'] + ])->shouldBeCalled()->willReturn(['name' => 'operations/foo']); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $res = $this->database->updateDdl($statement); + $this->assertInstanceOf(LongRunningOperation::class, $res); + } + + /** + * @group spanneradmin + */ + public function testDrop() + { + $this->connection->dropDatabase([ + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ])->shouldBeCalled(); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->database->drop(); + } + + /** + * @group spanneradmin + */ + public function testDdl() + { + $ddl = ['create table users', 'create table posts']; + $this->connection->getDatabaseDDL([ + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ])->willReturn(['statements' => $ddl]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->assertEquals($ddl, $this->database->ddl()); + } + + /** + * @group spanneradmin + */ + public function testDdlNoResult() + { + $this->connection->getDatabaseDDL([ + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ])->willReturn([]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->assertEquals([], $this->database->ddl()); + } + + /** + * @group spanneradmin + */ + public function testIam() + { + $this->assertInstanceOf(Iam::class, $this->database->iam()); + } + + public function testSnapshot() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $res = $this->database->snapshot(); + $this->assertInstanceOf(Snapshot::class, $res); + } + + /** + * @expectedException BadMethodCallException + */ + public function testSnapshotMinReadTimestamp() + { + $this->database->snapshot(['minReadTimestamp' => 'foo']); + } + + /** + * @expectedException BadMethodCallException + */ + public function testSnapshotMaxStaleness() + { + $this->database->snapshot(['maxStaleness' => 'foo']); + } + + public function testRunTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $hasTransaction = false; + + $this->database->runTransaction(function (Transaction $t) use (&$hasTransaction) { + $hasTransaction = true; + }); + + $this->assertTrue($hasTransaction); + } + + public function testRunTransactionRetry() + { + $abort = new AbortedException('foo', 409, null, [ + [ + 'retryDelay' => [ + 'seconds' => 1, + 'nanos' => 0 + ] + ] + ]); + + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalledTimes(3) + ->willReturn(['id' => self::TRANSACTION]); + + $it = 0; + $this->connection->commit(Argument::any()) + ->shouldBeCalledTimes(3) + ->will(function() use (&$it, $abort) { + $it++; + if ($it <= 2) { + throw $abort; + } + + return ['commitTimestamp' => TransactionTest::TIMESTAMP]; + }); + + $this->refreshOperation(); + + $this->database->runTransaction(function($t){$t->commit();}); + } + + /** + * @expectedException Google\Cloud\Core\Exception\AbortedException + */ + public function testRunTransactionAborted() + { + $abort = new AbortedException('foo', 409, null, [ + [ + 'retryDelay' => [ + 'seconds' => 1, + 'nanos' => 0 + ] + ] + ]); + + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $it = 0; + $this->connection->commit(Argument::any()) + ->shouldBeCalled() + ->will(function() use (&$it, $abort) { + $it++; + if ($it <= 8) { + throw $abort; + } + + return ['commitTimestamp' => TransactionTest::TIMESTAMP]; + }); + + $this->refreshOperation(); + + $this->database->runTransaction(function($t){$t->commit();}); + } + + public function testTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $t = $this->database->transaction(); + $this->assertInstanceOf(Transaction::class, $t); + } + + public function testInsert() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->insert($table, $row); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testInsertBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->insertBatch($table, [$row]); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testUpdate() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->update($table, $row); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testUpdateBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->updateBatch($table, [$row]); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testInsertOrUpdate() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->insertOrUpdate($table, $row); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testInsertOrUpdateBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->insertOrUpdateBatch($table, [$row]); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testReplace() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_REPLACE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->replace($table, $row); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testReplaceBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_REPLACE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->replaceBatch($table, [$row]); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testDelete() + { + $table = 'foo'; + $keys = [10, 'bar']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $keys) { + if ($arg['mutations'][0][Operation::OP_DELETE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][0] !== (string) $keys[0]) return false; + if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][1] !== $keys[1]) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->delete($table, new KeySet(['keys' => $keys])); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Table'; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { + if ($arg['sql'] !== $sql) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->database->execute($sql); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $table = 'Table'; + $opts = ['foo' => 'bar']; + + $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { + if ($arg['table'] !== $table) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['ID']) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->database->read($table, new KeySet(['all' => true]), ['ID']); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + // ******* + // Helpers + + private function refreshOperation() + { + $operation = new Operation($this->connection->reveal(), false); + $this->database->___setProperty('operation', $operation); + } + + private function commitResponse() + { + return ['commitTimestamp' => '2017-01-09T18:05:22.534799Z']; + } + + private function assertTimestampIsCorrect($res) + { + $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']); + + $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z')); + } +} diff --git a/tests/unit/Spanner/DateTest.php b/tests/unit/Spanner/DateTest.php new file mode 100644 index 000000000000..1c25c6bc3245 --- /dev/null +++ b/tests/unit/Spanner/DateTest.php @@ -0,0 +1,55 @@ +dt = new \DateTime('1989-10-11'); + $this->date = new Date($this->dt); + } + + public function testGet() + { + $this->assertEquals($this->dt, $this->date->get()); + } + + public function testFormatAsString() + { + $this->assertEquals($this->dt->format(Date::FORMAT), $this->date->formatAsString()); + } + + public function testCast() + { + $this->assertEquals($this->dt->format(Date::FORMAT), (string)$this->date); + } + + public function testType() + { + $this->assertTrue(is_integer($this->date->type())); + } +} diff --git a/tests/unit/Spanner/DurationTest.php b/tests/unit/Spanner/DurationTest.php new file mode 100644 index 000000000000..37d2575e42f7 --- /dev/null +++ b/tests/unit/Spanner/DurationTest.php @@ -0,0 +1,65 @@ +duration = new Duration(self::SECONDS, self::NANOS); + } + + public function testGet() + { + $this->assertEquals([ + 'seconds' => self::SECONDS, + 'nanos' => self::NANOS + ], $this->duration->get()); + } + + public function testType() + { + $this->assertEquals(Duration::TYPE, $this->duration->type()); + } + + public function testFormatAsString() + { + $this->assertEquals( + json_encode($this->duration->get()), + $this->duration->formatAsString() + ); + } + + public function testTostring() + { + $this->assertEquals( + json_encode($this->duration->get()), + (string)$this->duration + ); + } +} diff --git a/tests/unit/Spanner/InstanceTest.php b/tests/unit/Spanner/InstanceTest.php new file mode 100644 index 000000000000..5612ce712733 --- /dev/null +++ b/tests/unit/Spanner/InstanceTest.php @@ -0,0 +1,318 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->instance = \Google\Cloud\Dev\stub(Instance::class, [ + $this->connection->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), + $this->prophesize(LongRunningConnectionInterface::class)->reveal(), + [], + self::PROJECT_ID, + self::NAME + ], [ + 'info', + 'connection' + ]); + } + + public function testName() + { + $this->assertEquals(self::NAME, $this->instance->name()); + } + + public function testInfo() + { + $this->connection->getInstance()->shouldNotBeCalled(); + + $this->instance->___setProperty('info', ['foo' => 'bar']); + $this->assertEquals('bar', $this->instance->info()['foo']); + } + + public function testInfoWithReload() + { + $instance = $this->getDefaultInstance(); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $info = $this->instance->info(); + $this->assertEquals('Instance Name', $info['displayName']); + + $this->assertEquals($info, $this->instance->info()); + } + + public function testExists() + { + $this->connection->getInstance(Argument::any())->shouldBeCalled()->willReturn([]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->assertTrue($this->instance->exists()); + } + + public function testExistsNotFound() + { + $this->connection->getInstance(Argument::any()) + ->shouldBeCalled() + ->willThrow(new NotFoundException('foo', 404)); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->assertFalse($this->instance->exists()); + } + + public function testReload() + { + $instance = $this->getDefaultInstance(); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $info = $this->instance->reload(); + + $this->assertEquals('Instance Name', $info['displayName']); + } + + public function testState() + { + $instance = $this->getDefaultInstance(); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->assertEquals(Instance::STATE_READY, $this->instance->state()); + } + + public function testStateIsNull() + { + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn([]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->assertNull($this->instance->state()); + } + + public function testUpdate() + { + $instance = $this->getDefaultInstance(); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->connection->updateInstance([ + 'name' => $instance['name'], + 'displayName' => $instance['displayName'], + 'nodeCount' => $instance['nodeCount'], + 'labels' => [], + ])->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->instance->update(); + } + + public function testUpdateWithExistingLabels() + { + $instance = $this->getDefaultInstance(); + $instance['labels'] = ['foo' => 'bar']; + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->connection->updateInstance([ + 'name' => $instance['name'], + 'displayName' => $instance['displayName'], + 'nodeCount' => $instance['nodeCount'], + 'labels' => $instance['labels'], + ])->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->instance->update(); + } + + public function testUpdateWithChanges() + { + $instance = $this->getDefaultInstance(); + + $changes = [ + 'labels' => [ + 'foo' => 'bar' + ], + 'nodeCount' => 900, + 'displayName' => 'New Name', + ]; + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->connection->updateInstance([ + 'name' => $instance['name'], + 'displayName' => $changes['displayName'], + 'nodeCount' => $changes['nodeCount'], + 'labels' => $changes['labels'], + ])->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->instance->update($changes); + } + + public function testDelete() + { + $this->connection->deleteInstance([ + 'name' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME) + ])->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->instance->delete(); + } + + public function testCreateDatabase() + { + $extra = ['foo', 'bar']; + + $this->connection->createDatabase([ + 'instance' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME), + 'createStatement' => 'CREATE DATABASE `test-database`', + 'extraStatements' => $extra + ]) + ->shouldBeCalled() + ->willReturn(['name' => 'operations/foo']); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $database = $this->instance->createDatabase('test-database', [ + 'statements' => $extra + ]); + + $this->assertInstanceOf(LongRunningOperation::class, $database); + } + + public function testDatabase() + { + $database = $this->instance->database('test-database'); + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals('test-database', $database->name()); + } + + public function testDatabases() + { + $databases = [ + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')], + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] + ]; + + $this->connection->listDatabases(Argument::any()) + ->shouldBeCalled() + ->willReturn(['databases' => $databases]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $dbs = $this->instance->databases(); + + $this->assertInstanceOf(ItemIterator::class, $dbs); + + $dbs = iterator_to_array($dbs); + + $this->assertEquals(2, count($dbs)); + $this->assertEquals('database1', $dbs[0]->name()); + $this->assertEquals('database2', $dbs[1]->name()); + } + + public function testDatabasesPaged() + { + $databases = [ + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')], + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] + ]; + + $iteration = 0; + $this->connection->listDatabases(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn(['databases' => [$databases[0]], 'nextPageToken' => 'foo'], ['databases' => [$databases[1]]]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $dbs = $this->instance->databases(); + + $this->assertInstanceOf(ItemIterator::class, $dbs); + + $dbs = iterator_to_array($dbs); + + $this->assertEquals(2, count($dbs)); + $this->assertEquals('database1', $dbs[0]->name()); + $this->assertEquals('database2', $dbs[1]->name()); + } + + public function testIam() + { + $this->assertInstanceOf(Iam::class, $this->instance->iam()); + } + + // ************** // + + private function getDefaultInstance() + { + return json_decode(file_get_contents(__DIR__ .'/../fixtures/spanner/instance.json'), true); + } +} diff --git a/tests/unit/Spanner/KeyRangeTest.php b/tests/unit/Spanner/KeyRangeTest.php new file mode 100644 index 000000000000..f3903ce6aa7d --- /dev/null +++ b/tests/unit/Spanner/KeyRangeTest.php @@ -0,0 +1,95 @@ +range = new KeyRange; + } + + public function testGetters() + { + $range = new KeyRange([ + 'startType' => KeyRange::TYPE_CLOSED, + 'start' => ['foo'], + 'endType' => KeyRange::TYPE_OPEN, + 'end' => ['bar'] + ]); + + $this->assertEquals(['foo'], $range->start()); + $this->assertEquals(['bar'], $range->end()); + $this->assertEquals(['start' => KeyRange::TYPE_CLOSED, 'end' => KeyRange::TYPE_OPEN], $range->types()); + } + + public function testSetStart() + { + $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']); + $this->assertEquals(['foo'], $this->range->start()); + $this->assertEquals('startOpen', $this->range->types()['start']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testSetStartInvalidType() + { + $this->range->setStart('foo', ['foo']); + } + + public function testSetEnd() + { + $this->range->setEnd(KeyRange::TYPE_OPEN, ['foo']); + $this->assertEquals(['foo'], $this->range->end()); + $this->assertEquals('endOpen', $this->range->types()['end']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testSetEndInvalidType() + { + $this->range->setEnd('foo', ['foo']); + } + + public function testKeyRangeObject() + { + $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']); + $this->range->setEnd(KeyRange::TYPE_CLOSED, ['bar']); + + $res = $this->range->keyRangeObject(); + + $this->assertEquals(['startOpen' => ['foo'], 'endClosed' => ['bar']], $res); + } + + /** + * @expectedException BadMethodCallException + */ + public function testKeyRangeObjectBadRange() + { + $this->range->keyRangeObject(); + } +} diff --git a/tests/unit/Spanner/KeySetTest.php b/tests/unit/Spanner/KeySetTest.php new file mode 100644 index 000000000000..75551a6dc946 --- /dev/null +++ b/tests/unit/Spanner/KeySetTest.php @@ -0,0 +1,119 @@ +prophesize(KeyRange::class); + $range->keyRangeObject()->willReturn('foo'); + + $set->addRange($range->reveal()); + + $this->assertEquals('foo', $set->keySetObject()['ranges'][0]); + } + + public function testSetRanges() + { + $set = new KeySet; + + $range1 = $this->prophesize(KeyRange::class); + $range1->keyRangeObject()->willReturn('foo'); + + $range2 = $this->prophesize(KeyRange::class); + $range2->keyRangeObject()->willReturn('bar'); + + $ranges = [ + $range1->reveal(), + $range2->reveal() + ]; + + $set->setRanges($ranges); + + $this->assertEquals('foo', $set->keySetObject()['ranges'][0]); + $this->assertEquals('bar', $set->keySetObject()['ranges'][1]); + } + + public function testAddKey() + { + $set = new KeySet; + + $key = 'key'; + + $set->addKey($key); + + $this->assertEquals($key, $set->keySetObject()['keys'][0]); + } + + public function testSetKeys() + { + $set = new KeySet; + + $keys = ['key1','key2']; + + $set->setKeys($keys); + + $this->assertEquals($keys, $set->keySetObject()['keys']); + } + + public function testSetMatchAll() + { + $set = new KeySet; + + $set->setMatchAll(true); + $this->assertTrue($set->keySetObject()['all']); + + $set->setMatchAll(false); + $this->assertFalse($set->keySetObject()['all']); + } + + public function testRanges() + { + $set = new KeySet; + $range = $this->prophesize(KeyRange::class)->reveal(); + + $set->addRange($range); + $this->assertEquals($range, $set->ranges()[0]); + } + + public function testKeys() + { + $set = new KeySet; + $key = 'foo'; + $set->addKey($key); + + $this->assertEquals($key, $set->keys()[0]); + } + + public function testMatchAll() + { + $set = new KeySet(); + $this->assertFalse($set->matchAll()); + + $set->setMatchAll(true); + $this->assertTrue($set->matchAll()); + } +} diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php new file mode 100644 index 000000000000..298e8531632a --- /dev/null +++ b/tests/unit/Spanner/OperationTest.php @@ -0,0 +1,303 @@ +connection = $this->prophesize(ConnectionInterface::class); + + $this->operation = \Google\Cloud\Dev\stub(Operation::class, [ + $this->connection->reveal(), + false + ]); + + $session = $this->prophesize(Session::class); + $session->name()->willReturn(self::SESSION); + $this->session = $session->reveal(); + } + + public function testMutation() + { + $res = $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]); + + $this->assertEquals(Operation::OP_INSERT, array_keys($res)[0]); + $this->assertEquals('Posts', $res[Operation::OP_INSERT]['table']); + $this->assertEquals('foo', $res[Operation::OP_INSERT]['columns'][0]); + $this->assertEquals('bar', $res[Operation::OP_INSERT]['values'][0]); + } + + public function testDeleteMutation() + { + $keys = ['foo', 'bar']; + $range = new KeyRange([ + 'startType' => KeyRange::TYPE_CLOSED, + 'start' => ['foo'], + 'endType' => KeyRange::TYPE_OPEN, + 'end' => ['bar'] + ]); + + $keySet = new KeySet([ + 'keys' => $keys, + 'ranges' => [$range] + ]); + + $res = $this->operation->deleteMutation('Posts', $keySet); + + $this->assertEquals('Posts', $res['delete']['table']); + $this->assertEquals($keys, $res['delete']['keySet']['keys']); + $this->assertEquals($range->keyRangeObject(), $res['delete']['keySet']['ranges'][0]); + } + + public function testCommit() + { + $mutations = [ + $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]) + ]; + + $this->connection->commit(Argument::that(function ($arg) use ($mutations) { + if ($arg['mutations'] !== $mutations) return false; + if ($arg['transactionId'] !== 'foo') return false; + + return true; + }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->commit($this->session, $mutations, [ + 'transactionId' => 'foo' + ]); + + $this->assertInstanceOf(Timestamp::class, $res); + } + + public function testCommitWithExistingTransaction() + { + $mutations = [ + $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]) + ]; + + $this->connection->commit(Argument::that(function ($arg) use ($mutations) { + if ($arg['mutations'] !== $mutations) return false; + if (isset($arg['singleUseTransaction'])) return false; + if ($arg['transactionId'] !== self::TRANSACTION) return false; + + return true; + }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->commit($this->session, $mutations, [ + 'transactionId' => self::TRANSACTION + ]); + + $this->assertInstanceOf(Timestamp::class, $res); + } + + public function testRollback() + { + $this->connection->rollback(Argument::that(function ($arg) { + if ($arg['transactionId'] !== self::TRANSACTION) return false; + if ($arg['session'] !== self::SESSION) return false; + + return true; + }))->shouldBeCalled(); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $this->operation->rollback($this->session, self::TRANSACTION); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Posts WHERE ID = @id'; + $params = ['id' => 10]; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql, $params) { + if ($arg['sql'] !== $sql) return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['params'] !== ['id' => '10']) return false; + if ($arg['paramTypes']['id']['code'] !== ValueMapper::TYPE_INT64) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->execute($this->session, $sql, [ + 'parameters' => $params + ]); + + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo']); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testReadWithTransaction() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse([ + 'transaction' => ['id' => self::TRANSACTION] + ])); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ + 'transactionContext' => SessionPoolInterface::CONTEXT_READWRITE + ]); + $this->assertInstanceOf(Transaction::class, $res->transaction()); + $this->assertEquals(self::TRANSACTION, $res->transaction()->id()); + } + + public function testReadWithSnapshot() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse([ + 'transaction' => ['id' => self::TRANSACTION] + ])); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ + 'transactionContext' => SessionPoolInterface::CONTEXT_READ + ]); + $this->assertInstanceOf(Snapshot::class, $res->snapshot()); + $this->assertEquals(self::TRANSACTION, $res->snapshot()->id()); + } + + public function testTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $t = $this->operation->transaction($this->session); + $this->assertInstanceOf(Transaction::class, $t); + $this->assertEquals(self::TRANSACTION, $t->id()); + } + + public function testSnapshot() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snap = $this->operation->snapshot($this->session); + $this->assertInstanceOf(Snapshot::class, $snap); + $this->assertEquals(self::TRANSACTION, $snap->id()); + } + + public function testSnapshotWithTimestamp() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION, 'readTimestamp' => self::TIMESTAMP]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snap = $this->operation->snapshot($this->session); + $this->assertInstanceOf(Snapshot::class, $snap); + $this->assertEquals(self::TRANSACTION, $snap->id()); + $this->assertInstanceOf(Timestamp::class, $snap->readTimestamp()); + } + + private function executeAndReadResponse(array $additionalMetadata = []) + { + return [ + 'metadata' => array_merge([ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], $additionalMetadata), + 'rows' => [ + ['10'] + ] + ]; + } +} diff --git a/tests/unit/Spanner/ResultTest.php b/tests/unit/Spanner/ResultTest.php new file mode 100644 index 000000000000..98fa6d8ef6b5 --- /dev/null +++ b/tests/unit/Spanner/ResultTest.php @@ -0,0 +1,106 @@ + 'John'] + ]); + + $res = iterator_to_array($result); + $this->assertEquals(1, count($res)); + $this->assertEquals('John', $res[0]['name']); + } + + public function testMetadata() + { + $result = new Result(['metadata' => 'foo'], []); + $this->assertEquals('foo', $result->metadata()); + } + + public function testRows() + { + $rows = [ + ['name' => 'John'] + ]; + + $result = new Result([], $rows); + + $this->assertEquals($rows, $result->rows()); + } + + public function testFirstRow() + { + $rows = [ + ['name' => 'John'], + ['name' => 'Dave'] + ]; + + $result = new Result([], $rows); + + $this->assertEquals($rows[0], $result->firstRow()); + } + + public function testStats() + { + $result = new Result(['stats' => 'foo'], []); + $this->assertEquals('foo', $result->stats()); + } + + public function testInfo() + { + $info = ['foo' => 'bar']; + $result = new Result($info, []); + + $this->assertEquals($info, $result->info()); + } + + public function testTransaction() + { + $result = new Result([], [], [ + 'transaction' => 'foo' + ]); + + $this->assertEquals('foo', $result->transaction()); + + $result = new Result([], []); + + $this->assertNull($result->transaction()); + } + + public function testSnapshot() + { + $result = new Result([], [], [ + 'snapshot' => 'foo' + ]); + + $this->assertEquals('foo', $result->snapshot()); + + $result = new Result([], []); + + $this->assertNull($result->snapshot()); + } +} diff --git a/tests/unit/Spanner/SnapshotTest.php b/tests/unit/Spanner/SnapshotTest.php new file mode 100644 index 000000000000..cf54179ce9ed --- /dev/null +++ b/tests/unit/Spanner/SnapshotTest.php @@ -0,0 +1,48 @@ +timestamp = new Timestamp(new \DateTime); + $this->snapshot = new Snapshot( + $this->prophesize(Operation::class)->reveal(), + $this->prophesize(Session::class)->reveal(), + 'foo', + $this->timestamp + ); + } + + public function testReadTimestamp() + { + $this->assertEquals($this->timestamp, $this->snapshot->readTimestamp()); + } +} diff --git a/tests/unit/Spanner/SpannerClientTest.php b/tests/unit/Spanner/SpannerClientTest.php new file mode 100644 index 000000000000..b58b8ff37b90 --- /dev/null +++ b/tests/unit/Spanner/SpannerClientTest.php @@ -0,0 +1,288 @@ +markTestSkipped('Must have the grpc extension installed to run this test.'); + } + + $this->connection = $this->prophesize(ConnectionInterface::class); + $this->client = \Google\Cloud\Dev\stub(SpannerClient::class, [ + ['projectId' => self::PROJECT] + ]); + } + + /** + * @group spanneradmin + */ + public function testConfigurations() + { + $this->connection->listConfigs(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instanceConfigs' => [ + [ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => 'Bar' + ], [ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => 'Bat' + ] + ] + ]); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $configs = $this->client->configurations(); + + $this->assertInstanceOf(ItemIterator::class, $configs); + + $configs = iterator_to_array($configs); + $this->assertEquals(2, count($configs)); + $this->assertInstanceOf(Configuration::class, $configs[0]); + $this->assertInstanceOf(Configuration::class, $configs[1]); + } + + /** + * @group spanneradmin + */ + public function testPagedConfigurations() + { + $firstCall = [ + 'instanceConfigs' => [ + [ + 'name' => 'projects/foo/instanceConfigs/bar', + 'displayName' => 'Bar' + ] + ], + 'nextPageToken' => 'fooBar' + ]; + + $secondCall = [ + 'instanceConfigs' => [ + [ + 'name' => 'projects/foo/instanceConfigs/bat', + 'displayName' => 'Bat' + ] + ] + ]; + + $this->connection->listConfigs(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn($firstCall, $secondCall); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $configs = $this->client->configurations(); + + $this->assertInstanceOf(ItemIterator::class, $configs); + + $configs = iterator_to_array($configs); + $this->assertEquals(2, count($configs)); + $this->assertInstanceOf(Configuration::class, $configs[0]); + $this->assertInstanceOf(Configuration::class, $configs[1]); + } + + /** + * @group spanneradmin + */ + public function testConfiguration() + { + $config = $this->client->configuration('bar'); + + $this->assertInstanceOf(Configuration::class, $config); + $this->assertEquals('bar', $config->name()); + } + + /** + * @group spanneradmin + */ + public function testCreateInstance() + { + $this->connection->createInstance(Argument::that(function ($arg) { + if ($arg['name'] !== 'projects/'. self::PROJECT .'/instances/'. self::INSTANCE) return false; + if ($arg['config'] !== 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG) return false; + + return true; + })) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'operations/foo' + ]); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $config = $this->prophesize(Configuration::class); + $config->name()->willReturn(self::CONFIG); + + $operation = $this->client->createInstance($config->reveal(), self::INSTANCE); + + $this->assertInstanceOf(LongRunningOperation::class, $operation); + } + + /** + * @group spanneradmin + */ + public function testInstance() + { + $i = $this->client->instance('foo'); + $this->assertInstanceOf(Instance::class, $i); + $this->assertEquals('foo', $i->name()); + } + + /** + * @group spanneradmin + */ + public function testInstanceWithInstanceArray() + { + $i = $this->client->instance('foo', ['key' => 'val']); + $this->assertEquals('val', $i->info()['key']); + } + + /** + * @group spanneradmin + */ + public function testInstances() + { + $this->connection->listInstances(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instances' => [ + ['name' => 'projects/test-project/instances/foo'], + ['name' => 'projects/test-project/instances/bar'], + ] + ]); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $instances = $this->client->instances(); + $this->assertInstanceOf(ItemIterator::class, $instances); + + $instances = iterator_to_array($instances); + $this->assertEquals(2, count($instances)); + $this->assertEquals('foo', $instances[0]->name()); + $this->assertEquals('bar', $instances[1]->name()); + } + + /** + * @group spanneradmin + */ + public function testResumeOperation() + { + $opName = 'operations/foo'; + + $op = $this->client->resumeOperation($opName); + $this->assertInstanceOf(LongRunningOperation::class, $op); + $this->assertEquals($op->name(), $opName); + } + + public function testConnect() + { + $database = $this->client->connect(self::INSTANCE, self::DATABASE); + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals(self::DATABASE, $database->name()); + } + + public function testConnectWithInstance() + { + $inst = $this->client->instance(self::INSTANCE); + $database = $this->client->connect($inst, self::DATABASE); + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals(self::DATABASE, $database->name()); + } + + public function testKeyset() + { + $ks = $this->client->keySet(); + $this->assertInstanceOf(KeySet::class, $ks); + } + + public function testKeyRange() + { + $kr = $this->client->keyRange(); + $this->assertInstanceOf(KeyRange::class, $kr); + } + + public function testBytes() + { + $b = $this->client->bytes('foo'); + $this->assertInstanceOf(Bytes::class, $b); + $this->assertEquals(base64_encode('foo'), (string)$b); + } + + public function testDate() + { + $d = $this->client->date(new \DateTime); + $this->assertInstanceOf(Date::class, $d); + } + + public function testTimestamp() + { + $ts = $this->client->timestamp(new \DateTime); + $this->assertInstanceOf(Timestamp::class, $ts); + } + + public function testInt64() + { + $i64 = $this->client->int64('123'); + $this->assertInstanceOf(Int64::class, $i64); + } + + public function testDuration() + { + $d = $this->client->duration(10, 1); + $this->assertInstanceOf(Duration::class, $d); + } + + public function testSessionClient() + { + $sc = $this->client->sessionClient(); + $this->assertInstanceOf(SessionClient::class, $sc); + } +} diff --git a/tests/unit/Spanner/TimestampTest.php b/tests/unit/Spanner/TimestampTest.php new file mode 100644 index 000000000000..c18ee94e95c2 --- /dev/null +++ b/tests/unit/Spanner/TimestampTest.php @@ -0,0 +1,61 @@ +dt = new \DateTime('1989-10-11 08:58:00 +00:00'); + $this->ts = new Timestamp($this->dt); + } + + public function testGet() + { + $this->assertEquals($this->dt, $this->ts->get()); + } + + public function testFormatAsString() + { + $this->assertEquals( + (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'), + (new \DateTime($this->ts->formatAsString()))->format('U') + ); + } + + public function testCast() + { + $this->assertEquals( + (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'), + (new \DateTime((string)$this->ts))->format('U') + ); + } + + public function testType() + { + $this->assertTrue(is_integer($this->ts->type())); + } +} diff --git a/tests/unit/Spanner/TransactionConfigurationTraitTest.php b/tests/unit/Spanner/TransactionConfigurationTraitTest.php new file mode 100644 index 000000000000..2dd3f408e0b3 --- /dev/null +++ b/tests/unit/Spanner/TransactionConfigurationTraitTest.php @@ -0,0 +1,185 @@ +impl = new TransactionConfigurationTraitImplementation; + $this->ts = new Timestamp(new \DateTime(self::TIMESTAMP), self::NANOS); + $this->duration = new Duration(10,1); + $this->dur = ['seconds' => 10, 'nanos' => 1]; + } + + public function testTransactionSelectorBasicSnapshot() + { + $args = []; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertTrue($res[0]['singleUse']['readOnly']['strong']); + } + + public function testTransactionSelectorExistingId() + { + $args = ['transactionId' => self::TRANSACTION]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertEquals(self::TRANSACTION, $res[0]['id']); + } + + public function testTransactionSelectorReadWrite() + { + $args = ['transactionType' => SessionPoolInterface::CONTEXT_READWRITE]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $res[1]); + $this->assertEquals($this->impl->proxyConfigureTransactionOptions(), $res[0]['singleUse']); + } + + public function testBegin() + { + $args = ['begin' => true]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertTrue($res[0]['begin']['readOnly']['strong']); + } + + public function testConfigureSnapshotOptionsReturnReadTimestamp() + { + $args = ['returnReadTimestamp' => true]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertTrue($res['readOnly']['returnReadTimestamp']); + } + + public function testConfigureSnapshotOptionsStrong() + { + $args = ['strong' => true]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertTrue($res['readOnly']['strong']); + } + + public function testConfigureSnapshotOptionsMinReadTimestamp() + { + $args = ['minReadTimestamp' => $this->ts]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals(self::TIMESTAMP, $res['readOnly']['minReadTimestamp']); + } + + public function testConfigureSnapshotOptionsReadTimestamp() + { + $args = ['readTimestamp' => $this->ts]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals(self::TIMESTAMP, $res['readOnly']['readTimestamp']); + } + + public function testConfigureSnapshotOptionsMaxStaleness() + { + $args = ['maxStaleness' => $this->duration]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals($this->dur, $res['readOnly']['maxStaleness']); + } + + public function testConfigureSnapshotOptionsExactStaleness() + { + $args = ['exactStaleness' => $this->duration]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals($this->dur, $res['readOnly']['exactStaleness']); + } + + /** + * @expectedException BadMethodCallException + */ + public function testTransactionSelectorInvalidContext() + { + $args = ['transactionType' => 'foo']; + $this->impl->proxyTransactionSelector($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidExactStaleness() + { + $args = ['exactStaleness' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidMaxStaleness() + { + $args = ['maxStaleness' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidMinReadTimestamp() + { + $args = ['minReadTimestamp' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidReadTimestamp() + { + $args = ['readTimestamp' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } +} + +class TransactionConfigurationTraitImplementation +{ + use TransactionConfigurationTrait; + + public function proxyTransactionSelector(array &$options) + { + return $this->transactionSelector($options); + } + + public function proxyConfigureTransactionOptions() + { + return $this->configureTransactionOptions(); + } + + public function proxyConfigureSnapshotOptions(array &$options) + { + return $this->configureSnapshotOptions($options); + } +} diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php new file mode 100644 index 000000000000..867ae378dbb2 --- /dev/null +++ b/tests/unit/Spanner/TransactionTest.php @@ -0,0 +1,324 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->operation = new Operation($this->connection->reveal(), false); + + $this->session = new Session( + $this->connection->reveal(), + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + ); + + $args = [ + $this->operation, + $this->session, + self::TRANSACTION, + ]; + + $props = [ + 'operation', 'readTimestamp', 'state' + ]; + + $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props); + } + + public function testInsert() + { + $this->transaction->insert('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + } + + public function testInsertBatch() + { + $this->transaction->insertBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + } + + public function testUpdate() + { + $this->transaction->update('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['update']['table']); + $this->assertEquals('foo', $mutations[0]['update']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['update']['values'][0]); + } + + public function testUpdateBatch() + { + $this->transaction->updateBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['update']['table']); + $this->assertEquals('foo', $mutations[0]['update']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['update']['values'][0]); + } + + public function testInsertOrUpdate() + { + $this->transaction->insertOrUpdate('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insertOrUpdate']['table']); + $this->assertEquals('foo', $mutations[0]['insertOrUpdate']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insertOrUpdate']['values'][0]); + } + + public function testInsertOrUpdateBatch() + { + $this->transaction->insertOrUpdateBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insertOrUpdate']['table']); + $this->assertEquals('foo', $mutations[0]['insertOrUpdate']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insertOrUpdate']['values'][0]); + } + + public function testReplace() + { + $this->transaction->replace('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['replace']['table']); + $this->assertEquals('foo', $mutations[0]['replace']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['replace']['values'][0]); + } + + public function testReplaceBatch() + { + $this->transaction->replaceBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['replace']['table']); + $this->assertEquals('foo', $mutations[0]['replace']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['replace']['values'][0]); + } + + public function testDelete() + { + $this->transaction->delete('Posts', new KeySet(['keys' => ['foo']])); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertEquals('Posts', $mutations[0]['delete']['table']); + $this->assertEquals('foo', $mutations[0]['delete']['keySet']['keys'][0]); + $this->assertFalse($mutations[0]['delete']['keySet']['all']); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Table'; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { + if ($arg['transaction']['id'] !== self::TRANSACTION) return false; + if ($arg['sql'] !== $sql) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->transaction->execute($sql); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $table = 'Table'; + $opts = ['foo' => 'bar']; + + $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { + if ($arg['transaction']['id'] !== self::TRANSACTION) return false; + if ($arg['table'] !== $table) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['ID']) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->transaction->read($table, new KeySet(['all' => true]), ['ID']); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testCommit() + { + $this->transaction->insert('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $operation = $this->prophesize(Operation::class); + $operation->commit($this->session, $mutations, ['transactionId' => self::TRANSACTION])->shouldBeCalled(); + + $this->transaction->___setProperty('operation', $operation->reveal()); + + $this->transaction->commit(); + } + + /** + * @expectedException RuntimeException + */ + public function testCommitInvalidState() + { + $this->transaction->___setProperty('state', 'foo'); + $this->transaction->commit(); + } + + public function testRollback() + { + $this->connection->rollback(Argument::any()) + ->shouldBeCalled(); + + $this->refreshOperation(); + + $this->transaction->rollback(); + } + + /** + * @expectedException RuntimeException + */ + public function testRollbackInvalidState() + { + $this->transaction->___setProperty('state', 'foo'); + $this->transaction->rollback(); + } + + public function testId() + { + $this->assertEquals(self::TRANSACTION, $this->transaction->id()); + } + + public function testState() + { + $this->assertEquals(Transaction::STATE_ACTIVE, $this->transaction->state()); + + $this->transaction->___setProperty('state', Transaction::STATE_COMMITTED); + $this->assertEquals(Transaction::STATE_COMMITTED, $this->transaction->state()); + } + + // ******* + // Helpers + + private function refreshOperation() + { + $operation = new Operation($this->connection->reveal(), false); + $this->transaction->___setProperty('operation', $operation); + } + + private function commitResponse() + { + return ['commitTimestamp' => self::TIMESTAMP]; + } + + private function assertTimestampIsCorrect($res) + { + $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']); + + $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z')); + } +} diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php new file mode 100644 index 000000000000..404ba6a3201e --- /dev/null +++ b/tests/unit/Spanner/ValueMapperTest.php @@ -0,0 +1,385 @@ +mapper = new ValueMapper(false); + } + + public function testFormatParamsForExecuteSqlSimpleTypes() + { + $params = [ + 'id' => 1, + 'name' => 'john', + 'pi' => 3.1515, + 'isCool' => false, + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($params, $res['params']); + $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['id']['code']); + $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['name']['code']); + $this->assertEquals(ValueMapper::TYPE_FLOAT64, $res['paramTypes']['pi']['code']); + $this->assertEquals(ValueMapper::TYPE_BOOL, $res['paramTypes']['isCool']['code']); + } + + public function testFormatParamsForExecuteSqlResource() + { + $c = 'hello world'; + + $resource = fopen('php://temp', 'r+'); + fwrite($resource, $c); + rewind($resource); + + $params = [ + 'resource' => $resource + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($c, base64_decode($res['params']['resource'])); + $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['resource']['code']); + } + + public function testFormatParamsForExecuteSqlArray() + { + $params = [ + 'array' => ['foo', 'bar'] + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals('foo', $res['params']['array'][0]); + $this->assertEquals('bar', $res['params']['array'][1]); + $this->assertEquals(ValueMapper::TYPE_ARRAY, $res['paramTypes']['array']['code']); + $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['array']['arrayElementType']['code']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlArrayInvalidAssoc() + { + $this->mapper->formatParamsForExecuteSql(['array' => [ + 'foo' => 'bar' + ]]); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlInvalidTypes() + { + $this->mapper->formatParamsForExecuteSql(['array' => ['foo', 3.1515]]); + } + + public function testFormatParamsForExecuteSqlInt64() + { + $val = '1234'; + $params = [ + 'int' => new Int64($val) + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($val, $res['params']['int']); + $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['int']['code']); + } + + public function testFormatParamsForExecuteSqlValueInterface() + { + $val = 'hello world'; + $params = [ + 'bytes' => new Bytes($val) + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + $this->assertEquals($val, base64_decode($res['params']['bytes'])); + $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['bytes']['code']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlInvalidObjectType() + { + $params = [ + 'bad' => $this + ]; + + $this->mapper->formatParamsForExecuteSql($params); + } + + public function testEncodeValuesAsSimpleType() + { + $dt = new \DateTime; + + $vals = []; + $vals['bool'] = true; + $vals['int'] = 555555; + $vals['intObj'] = new Int64((string) $vals['int']); + $vals['float'] = 3.1415; + $vals['nan'] = NAN; + $vals['inf'] = INF; + $vals['timestamp'] = new Timestamp($dt); + $vals['date'] = new Date($dt); + $vals['string'] = 'foo'; + $vals['bytes'] = new Bytes('hello world'); + $vals['array'] = ['foo', 'bar']; + + $res = $this->mapper->encodeValuesAsSimpleType($vals); + + $this->assertTrue($res[0]); + $this->assertEquals((string) $vals['int'], $res[1]); + $this->assertEquals((string) $vals['int'], $res[2]); + $this->assertEquals($vals['float'], $res[3]); + $this->assertTrue(is_nan($res[4])); + $this->assertEquals(INF, $res[5]); + $this->assertEquals($dt->format(Timestamp::FORMAT), $res[6]); + $this->assertEquals($dt->format(Date::FORMAT), $res[7]); + $this->assertEquals($vals['string'], $res[8]); + $this->assertEquals(base64_encode('hello world'), $res[9]); + $this->assertEquals($vals['array'], $res[10]); + } + + public function testDecodeValuesBool() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_BOOL), + $this->createRow(false) + ); + $this->assertEquals(false, $res['rowName']); + } + + public function testDecodeValuesInt() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_INT64), + $this->createRow('555') + ); + $this->assertEquals(555, $res['rowName']); + } + + public function testDecodeValuesInt64Object() + { + $mapper = new ValueMapper(true); + $res = $mapper->decodeValues( + $this->createField(ValueMapper::TYPE_INT64), + $this->createRow('555') + ); + $this->assertInstanceOf(Int64::class, $res['rowName']); + $this->assertEquals('555', $res['rowName']->get()); + } + + public function testDecodeValuesFloat() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow(3.1415) + ); + $this->assertEquals(3.1415, $res['rowName']); + } + + public function testDecodeValuesFloatNaN() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('NaN') + ); + $this->assertTrue(is_nan($res['rowName'])); + } + + public function testDecodeValuesFloatInfinity() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('Infinity') + ); + + $this->assertTrue(is_infinite($res['rowName'])); + $this->assertTrue($res['rowName'] > 0); + } + + public function testDecodeValuesFloatNegativeInfinity() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('-Infinity') + ); + + $this->assertTrue(is_infinite($res['rowName'])); + $this->assertTrue($res['rowName'] < 0); + } + + /** + * @expectedException RuntimeException + */ + public function testDecodeValuesFloatError() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('foo') + ); + } + + public function testDecodeValuesString() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_STRING), + $this->createRow('foo') + ); + $this->assertEquals('foo', $res['rowName']); + } + + public function testDecodeValuesTimestamp() + { + $dt = new \DateTime; + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_TIMESTAMP), + $this->createRow($dt->format(Timestamp::FORMAT)) + ); + + $this->assertInstanceOf(Timestamp::class, $res['rowName']); + $this->assertEquals($dt->format(Timestamp::FORMAT), $res['rowName']->formatAsString()); + } + + public function testDecodeValuesDate() + { + $dt = new \DateTime; + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_DATE), + $this->createRow($dt->format(Date::FORMAT)) + ); + + $this->assertInstanceOf(Date::class, $res['rowName']); + $this->assertEquals($dt->format(Date::FORMAT), $res['rowName']->formatAsString()); + } + + public function testDecodeValuesBytes() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_BYTES), + $this->createRow(base64_encode('hello world')) + ); + + $this->assertInstanceOf(Bytes::class, $res['rowName']); + $this->assertEquals('hello world', $res['rowName']->get()); + } + + public function testDecodeValuesArray() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_ARRAY, 'arrayElementType', [ + 'code' => ValueMapper::TYPE_STRING + ]), $this->createRow(['foo', 'bar']) + ); + + $this->assertEquals('foo', $res['rowName'][0]); + $this->assertEquals('bar', $res['rowName'][1]); + } + + public function testDecodeValuesStruct() + { + $field = [ + 'name' => 'structTest', + 'type' => [ + 'code' => ValueMapper::TYPE_ARRAY, + 'arrayElementType' => [ + 'code' => ValueMapper::TYPE_STRUCT, + 'structType' => [ + 'fields' => [ + [ + 'name' => 'rowName', + 'type' => [ + 'code' => ValueMapper::TYPE_STRING + ] + ] + ] + ] + ] + ] + ]; + + $row = [ + [ + 'Hello World' + ] + ]; + + $res = $this->mapper->decodeValues( + [$field], + [$row] + ); + + $this->assertEquals('Hello World', $res['structTest'][0]['rowName']); + } + + public function testDecodeValuesAnonymousField() + { + $fields = [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64, + ] + ], [ + 'type' => [ + 'code' => ValueMapper::TYPE_STRING + ] + ] + ]; + + $row = ['1337', 'John']; + + $res = $this->mapper->decodeValues($fields, $row); + + $this->assertEquals('1337', $res['ID']); + $this->assertEquals('John', $res[1]); + } + + private function createField($code, $type = null, array $typeObj = []) + { + return [[ + 'name' => 'rowName', + 'type' => array_filter([ + 'code' => $code, + $type => $typeObj + ]) + ]]; + } + + private function createRow($val) + { + return [$val]; + } +} diff --git a/tests/unit/fixtures/spanner/instance.json b/tests/unit/fixtures/spanner/instance.json new file mode 100644 index 000000000000..fcf371769ce3 --- /dev/null +++ b/tests/unit/fixtures/spanner/instance.json @@ -0,0 +1,7 @@ +{ + "name": "projects\/test-project\/instances\/instance-name", + "config": "projects\/test-project\/instanceConfigs\/regional-europe-west1", + "displayName": "Instance Name", + "nodeCount": 1, + "state": 2 +} From 6636b40318fd3146b70525ef14d8c43288508a19 Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Fri, 7 Apr 2017 12:32:14 -0400 Subject: [PATCH 06/46] introduce cache based session pool and streaming execute/read --- composer.json | 6 +- src/Core/GrpcRequestWrapper.php | 24 +- src/Core/GrpcTrait.php | 2 +- src/Core/Lock/LockInterface.php | 47 ++ src/Core/Lock/SymfonyLockAdapter.php | 95 +++ src/Core/PhpArray.php | 23 +- src/Core/composer.json | 5 +- src/Spanner/Configuration.php | 5 +- .../Connection/ConnectionInterface.php | 6 +- src/Spanner/Connection/Grpc.php | 11 +- src/Spanner/Database.php | 279 +++++++-- src/Spanner/Duration.php | 2 +- src/Spanner/Instance.php | 25 +- src/Spanner/KeyRange.php | 5 +- src/Spanner/Operation.php | 111 ++-- src/Spanner/Result.php | 267 ++++++--- src/Spanner/Session/CacheSessionPool.php | 547 ++++++++++++++++++ src/Spanner/Session/Session.php | 42 +- src/Spanner/Session/SessionClient.php | 104 ---- src/Spanner/Session/SessionPool.php | 63 -- src/Spanner/Session/SessionPoolInterface.php | 31 +- src/Spanner/Session/SimpleSessionPool.php | 54 -- src/Spanner/Snapshot.php | 9 +- src/Spanner/SpannerClient.php | 45 +- src/Spanner/Transaction.php | 18 +- src/Spanner/ValueMapper.php | 10 +- tests/snippets/Spanner/DatabaseTest.php | 126 ++-- tests/snippets/Spanner/InstanceTest.php | 2 - tests/snippets/Spanner/ResultTest.php | 110 ++++ tests/snippets/Spanner/SnapshotTest.php | 29 +- tests/snippets/Spanner/SpannerClientTest.php | 10 - tests/snippets/Spanner/TransactionTest.php | 63 +- tests/snippets/Speech/SpeechClientTest.php | 4 +- tests/system/Spanner/OperationsTest.php | 4 +- tests/system/Spanner/SnapshotTest.php | 6 +- tests/unit/Core/PhpArrayTest.php | 2 +- tests/unit/Spanner/DatabaseTest.php | 69 +-- tests/unit/Spanner/InstanceTest.php | 1 - tests/unit/Spanner/OperationTest.php | 24 +- tests/unit/Spanner/ResultTest.php | 139 +++-- .../Spanner/Session/CacheSessionPoolTest.php | 471 +++++++++++++++ tests/unit/Spanner/SpannerClientTest.php | 7 - tests/unit/Spanner/TransactionTest.php | 72 +-- .../streaming-read-acceptance-test.json | 371 ++++++++++++ 44 files changed, 2574 insertions(+), 772 deletions(-) create mode 100644 src/Core/Lock/LockInterface.php create mode 100644 src/Core/Lock/SymfonyLockAdapter.php create mode 100644 src/Spanner/Session/CacheSessionPool.php delete mode 100644 src/Spanner/Session/SessionClient.php delete mode 100644 src/Spanner/Session/SessionPool.php delete mode 100644 src/Spanner/Session/SimpleSessionPool.php create mode 100644 tests/snippets/Spanner/ResultTest.php create mode 100644 tests/unit/Spanner/Session/CacheSessionPoolTest.php create mode 100644 tests/unit/fixtures/spanner/streaming-read-acceptance-test.json diff --git a/composer.json b/composer.json index 4731d19473da..8b061ebcd98f 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "natural language", "pubsub", "pub sub", + "spanner", "speech", "storage", "gcs", @@ -39,13 +40,14 @@ } ], "require": { - "php": ">=5.5", + "php": ">=5.5.9", "rize/uri-template": "~0.3", "google/auth": "^0.11", "guzzlehttp/guzzle": "^5.3|^6.0", "guzzlehttp/psr7": "^1.2", "monolog/monolog": "~1", - "psr/http-message": "1.0.*" + "psr/http-message": "1.0.*", + "symfony/lock": "dev-master" }, "require-dev": { "phpunit/phpunit": "4.8.*", diff --git a/src/Core/GrpcRequestWrapper.php b/src/Core/GrpcRequestWrapper.php index ad42cf7d85ea..e8fcd2fdb98f 100644 --- a/src/Core/GrpcRequestWrapper.php +++ b/src/Core/GrpcRequestWrapper.php @@ -29,6 +29,7 @@ use Google\GAX\OperationResponse; use Google\GAX\PagedListResponse; use Google\GAX\RetrySettings; +use Google\GAX\ServerStream; use Grpc; /** @@ -158,7 +159,7 @@ public function send(callable $request, array $args, array $options = []) * Serializes a gRPC response. * * @param mixed $response - * @return array|null + * @return \Generator|array|null */ private function handleResponse($response) { @@ -174,9 +175,30 @@ private function handleResponse($response) return $response; } + if ($response instanceof ServerStream) { + return $this->handleStream($response); + } + return null; } + /** + * Handles a streaming response. + * + * @param ServerStream $response + * @return \Generator|array|null + */ + private function handleStream(ServerStream $response) + { + try { + foreach ($response->readAll() as $count => $result) { + yield $result->serialize($this->codec); + } + } catch (\Exception $ex) { + throw $this->convertToGoogleException($ex); + } + } + /** * Convert a GAX exception to a Google Exception. * diff --git a/src/Core/GrpcTrait.php b/src/Core/GrpcTrait.php index 212f527c2e67..4fa5de20aa15 100644 --- a/src/Core/GrpcTrait.php +++ b/src/Core/GrpcTrait.php @@ -52,7 +52,7 @@ public function setRequestWrapper(GrpcRequestWrapper $requestWrapper) * * @param callable $request * @param array $args - * @return array + * @return \Generator|array */ public function send(callable $request, array $args) { diff --git a/src/Core/Lock/LockInterface.php b/src/Core/Lock/LockInterface.php new file mode 100644 index 000000000000..2c0c98bab090 --- /dev/null +++ b/src/Core/Lock/LockInterface.php @@ -0,0 +1,47 @@ +lock = $lock; + } + + /** + * Acquires a lock that will block until released. + * + * @return bool + * @throws \RuntimeException + */ + public function acquire() + { + try { + return $this->lock->acquire(true); + } catch (\Exception $ex) { + throw new \RunTimeException($ex->getMessage()); + } + } + + /** + * Releases the lock. + * + * @throws \RuntimeException + */ + public function release() + { + try { + $this->lock->release(); + } catch (\Exception $ex) { + throw new \RunTimeException($ex->getMessage()); + } + } + + /** + * Execute a callable within a lock. + * + * @return mixed + * @throws \RuntimeException + */ + public function synchronize(callable $func) + { + $result = null; + $exception = null; + + if ($this->acquire()) { + try { + $result = $func(); + } catch (\Exception $ex) { + $exception = $ex; + } + $this->release(); + } + + if ($exception) { + throw $exception; + } + + return $result; + } +} diff --git a/src/Core/PhpArray.php b/src/Core/PhpArray.php index a18ae19f0872..7829afbce919 100644 --- a/src/Core/PhpArray.php +++ b/src/Core/PhpArray.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Core; use DrSlump\Protobuf; +use google\protobuf\Value; use google\protobuf\ListValue; use google\protobuf\NullValue; use google\protobuf\Struct; @@ -180,7 +181,7 @@ protected function filterValue($value, Protobuf\Field $field) $field->getValue(), $field->descriptor()->getFieldByName('value') ); - $vals[$field->getKey()] = current($val); + $vals[$field->getKey()] = $val; } return $vals; @@ -190,11 +191,29 @@ protected function filterValue($value, Protobuf\Field $field) $vals = []; foreach ($value->getValuesList() as $val) { - $vals[] = current($this->encodeMessage($val)); + $fields = $val->descriptor()->getFields(); + + foreach ($fields as $field) { + $name = $field->getName(); + if ($val->$name) { + $vals[] = $this->filterValue($val->$name, $field); + } + } } return $vals; } + + if ($value instanceof Value) { + $fields = $value->descriptor()->getFields(); + + foreach ($fields as $field) { + $name = $field->getName(); + if ($value->$name) { + return $this->filterValue($value->$name, $field); + } + } + } } return parent::filterValue($value, $field); diff --git a/src/Core/composer.json b/src/Core/composer.json index cee03e142b4a..2d27262dc7c1 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -4,13 +4,14 @@ "license": "Apache-2.0", "minimum-stability": "stable", "require": { - "php": ">=5.5", + "php": ">=5.5.9", "rize/uri-template": "~0.3", "google/auth": "^0.11", "guzzlehttp/guzzle": "^5.3|^6.0", "guzzlehttp/psr7": "^1.2", "monolog/monolog": "~1", - "psr/http-message": "1.0.*" + "psr/http-message": "1.0.*", + "symfony/lock": "dev-master" }, "extra": { "component": { diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php index d8eea3e9aead..1fbc1da9a4b1 100644 --- a/src/Spanner/Configuration.php +++ b/src/Spanner/Configuration.php @@ -26,10 +26,9 @@ * * Example: * ``` - * use Google\Cloud\ServiceBuilder; + * use Google\Cloud\Spanner\SpannerClient; * - * $cloud = new ServiceBuilder(); - * $spanner = $cloud->spanner(); + * $spanner = new SpannerClient(); * * $configuration = $spanner->configuration('regional-europe-west'); * ``` diff --git a/src/Spanner/Connection/ConnectionInterface.php b/src/Spanner/Connection/ConnectionInterface.php index 70e28b043b5b..12de8323345a 100644 --- a/src/Spanner/Connection/ConnectionInterface.php +++ b/src/Spanner/Connection/ConnectionInterface.php @@ -129,13 +129,15 @@ public function deleteSession(array $args = []); /** * @param array $args [optional] + * @return \Generator */ - public function executeSql(array $args = []); + public function executeStreamingSql(array $args = []); /** * @param array $args [optional] + * @return \Generator */ - public function read(array $args = []); + public function streamingRead(array $args = []); /** * @param array $args [optional] diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index fdd69550d09e..532ea335599a 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -134,7 +134,6 @@ public function __construct(array $config = []) $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); $this->spannerClient = new SpannerClient($grpcConfig); $this->operationsClient = $this->instanceAdminClient->getOperationsClient(); - $this->longRunningGrpcClients = [ $this->instanceAdminClient, $this->databaseAdminClient @@ -399,8 +398,9 @@ public function deleteSession(array $args = []) /** * @param array $args [optional] + * @return \Generator */ - public function executeSql(array $args = []) + public function executeStreamingSql(array $args = []) { $params = $this->pluck('params', $args); if ($params) { @@ -415,7 +415,7 @@ public function executeSql(array $args = []) $args['transaction'] = $this->createTransactionSelector($args); - return $this->send([$this->spannerClient, 'executeSql'], [ + return $this->send([$this->spannerClient, 'executeStreamingSql'], [ $this->pluck('session', $args), $this->pluck('sql', $args), $args @@ -424,8 +424,9 @@ public function executeSql(array $args = []) /** * @param array $args [optional] + * @return \Generator */ - public function read(array $args = []) + public function streamingRead(array $args = []) { $keySet = $this->pluck('keySet', $args); $keySet = (new KeySet) @@ -433,7 +434,7 @@ public function read(array $args = []) $args['transaction'] = $this->createTransactionSelector($args); - return $this->send([$this->spannerClient, 'read'], [ + return $this->send([$this->spannerClient, 'streamingRead'], [ $this->pluck('session', $args), $this->pluck('table', $args), $this->pluck('columns', $args), diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 32ff682b2638..27a5f477fad3 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -26,28 +26,27 @@ use Google\Cloud\Core\Retry; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; +use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\V1\SpannerClient as GrpcSpannerClient; /** - * Represents a Google Cloud Spanner Database + * Represents a Google Cloud Spanner Database. * * Example: * ``` - * use Google\Cloud\ServiceBuilder; + * use Google\Cloud\Spanner\SpannerClient; * - * $cloud = new ServiceBuilder(); - * $spanner = $cloud->spanner(); + * $spanner = new SpannerClient(); * * $database = $spanner->connect('my-instance', 'my-database'); * ``` * * ``` * // Databases can also be connected to via an Instance. - * use Google\Cloud\ServiceBuilder; + * use Google\Cloud\Spanner\SpannerClient; * - * $cloud = new ServiceBuilder(); - * $spanner = $cloud->spanner(); + * $spanner = new SpannerClient(); * * $instance = $spanner->instance('my-instance'); * $database = $instance->database('my-database'); @@ -70,11 +69,6 @@ class Database */ private $instance; - /** - * @var SessionPoolInterface - */ - private $sessionPool; - /** * @var LongRunningConnectionInterface */ @@ -100,40 +94,54 @@ class Database */ private $iam; + /** + * @var Session|null + */ + private $session; + + /** + * @var SessionPoolInterface|null + */ + private $sessionPool; + /** * Create an object representing a Database. * * @param ConnectionInterface $connection The connection to the * Google Cloud Spanner Admin API. * @param Instance $instance The instance in which the database exists. - * @param SessionPoolInterface $sessionPool The session pool implementation. * @param LongRunningConnectionInterface $lroConnection An implementation * mapping to methods which handle LRO resolution in the service. * @param string $projectId The project ID. * @param string $name The database name. - * @param bool $returnInt64AsObject If true, 64 bit integers will be - * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit + * @param SessionPoolInterface $sessionPool [optional] The session pool + * implementation. + * @param bool $returnInt64AsObject [optional If true, 64 bit integers will + * be returned as a {@see Google\Cloud\Core\Int64} object for 32 bit * platform compatibility. **Defaults to** false. */ public function __construct( ConnectionInterface $connection, Instance $instance, - SessionPoolInterface $sessionPool, LongRunningConnectionInterface $lroConnection, array $lroCallables, $projectId, $name, + SessionPoolInterface $sessionPool = null, $returnInt64AsObject = false ) { $this->connection = $connection; $this->instance = $instance; - $this->sessionPool = $sessionPool; $this->lroConnection = $lroConnection; $this->lroCallables = $lroCallables; $this->projectId = $projectId; $this->name = $name; - + $this->sessionPool = $sessionPool; $this->operation = new Operation($connection, $returnInt64AsObject); + + if ($this->sessionPool) { + $this->sessionPool->setDatabase($this); + } } /** @@ -386,7 +394,11 @@ public function snapshot(array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); - return $this->operation->snapshot($session, $transactionOptions); + try { + return $this->operation->snapshot($session, $transactionOptions); + } finally { + $session->setExpiration(); + } } /** @@ -416,12 +428,13 @@ public function snapshot(array $options = []) * Example: * ``` * $transaction = $database->runTransaction(function (Transaction $t) use ($username, $password) { - * $user = $t->execute('SELECT * FROM Users WHERE Name = @name and PasswordHash = @password', [ + * $rows = $t->execute('SELECT * FROM Users WHERE Name = @name and PasswordHash = @password', [ * 'parameters' => [ * 'name' => $username, * 'password' => password_hash($password, PASSWORD_DEFAULT) * ] - * ])->firstRow(); + * ])->rows(); + * $user = $rows->current(); * * if ($user) { * // Do something here to grant the user access. @@ -500,7 +513,12 @@ public function runTransaction(callable $operation, array $options = []) }; $retry = new Retry($options['maxRetries'], $delayFn); - return $retry->execute($commitFn, [$operation, $session, $options]); + + try { + return $retry->execute($commitFn, [$operation, $session, $options]); + } finally { + $session->setExpiration(); + } } /** @@ -539,7 +557,12 @@ public function transaction(array $options = []) ]; $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - return $this->operation->transaction($session, $options); + + try { + return $this->operation->transaction($session, $options); + } finally { + $session->setExpiration(); + } } /** @@ -609,7 +632,12 @@ public function insertBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); $options['singleUseTransaction'] = $this->configureTransactionOptions(); - return $this->operation->commit($session, $mutations, $options); + + try { + return $this->operation->commit($session, $mutations, $options); + } finally { + $session->setExpiration(); + } } /** @@ -684,7 +712,12 @@ public function updateBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); $options['singleUseTransaction'] = $this->configureTransactionOptions(); - return $this->operation->commit($session, $mutations, $options); + + try { + return $this->operation->commit($session, $mutations, $options); + } finally { + $session->setExpiration(); + } } /** @@ -762,7 +795,12 @@ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); $options['singleUseTransaction'] = $this->configureTransactionOptions(); - return $this->operation->commit($session, $mutations, $options); + + try { + return $this->operation->commit($session, $mutations, $options); + } finally { + $session->setExpiration(); + } } /** @@ -840,7 +878,12 @@ public function replaceBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); $options['singleUseTransaction'] = $this->configureTransactionOptions(); - return $this->operation->commit($session, $mutations, $options); + + try { + return $this->operation->commit($session, $mutations, $options); + } finally { + $session->setExpiration(); + } } /** @@ -875,7 +918,12 @@ public function delete($table, KeySet $keySet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); $options['singleUseTransaction'] = $this->configureTransactionOptions(); - return $this->operation->commit($session, $mutations, $options); + + try { + return $this->operation->commit($session, $mutations, $options); + } finally { + $session->setExpiration(); + } } /** @@ -888,6 +936,10 @@ public function delete($table, KeySet $keySet, array $options = []) * 'postId' => 1337 * ] * ]); + * + * $firstRow = $result + * ->rows() + * ->current(); * ``` * * ``` @@ -899,6 +951,8 @@ public function delete($table, KeySet $keySet, array $options = []) * 'begin' => true * ]); * + * $result->rows()->current(); + * * $snapshot = $result->snapshot(); * ``` * @@ -912,6 +966,8 @@ public function delete($table, KeySet $keySet, array $options = []) * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE * ]); * + * $result->rows()->current(); + * * $transaction = $result->transaction(); * ``` * @@ -969,7 +1025,11 @@ public function execute($sql, array $options = []) $options['transaction'] = $transactionOptions; $options['transactionContext'] = $context; - return $this->operation->execute($session, $sql, $options); + try { + return $this->operation->execute($session, $sql, $options); + } finally { + $session->setExpiration(); + } } /** @@ -984,6 +1044,10 @@ public function execute($sql, array $options = []) * $columns = ['ID', 'title', 'content']; * * $result = $database->read('Posts', $keySet, $columns); + * + * $firstRow = $result + * ->rows() + * ->current(); * ``` * * ``` @@ -998,6 +1062,8 @@ public function execute($sql, array $options = []) * 'begin' => true * ]); * + * $result->rows()->current(); + * * $snapshot = $result->snapshot(); * ``` * @@ -1014,6 +1080,8 @@ public function execute($sql, array $options = []) * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE * ]); * + * $result->rows()->current(); + * * $transaction = $result->transaction(); * ``` * @@ -1071,38 +1139,125 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] $options['transaction'] = $transactionOptions; $options['transactionContext'] = $context; - return $this->operation->read($session, $table, $keySet, $columns, $options); + try { + return $this->operation->read($session, $table, $keySet, $columns, $options); + } finally { + $session->setExpiration(); + } + } + + /** + * Get the underlying session pool implementation. + * + * Example: + * ``` + * $pool = $database->sessionPool(); + * ``` + * + * @return SessionPoolInterface|null + */ + public function sessionPool() + { + return $this->sessionPool; + } + + /** + * Closes the database connection by returning the active session back to + * the session pool or by deleting the session if there is no pool + * associated. + * + * It is highly important to ensure this is called as it is not always safe + * to rely soley on {@see Google\Cloud\Spanner\Database::__destruct()}. + * + * Example: + * ``` + * $database->close(); + * ``` + */ + public function close() + { + if ($this->session) { + if ($this->sessionPool) { + $this->sessionPool->release($this->session); + } else { + $this->session->delete(); + } + + $this->session = null; + } } /** - * Retrieve a session from the session pool. + * Closes the database connection. + */ + public function __destruct() + { + try { + $this->close(); + } catch (\Exception $ex) { + } + } + + /** + * Create a new session. + * + * Sessions are handled behind the scenes and this method not need to be + * called directly. * - * @param string $context The session context. + * @access private + * @param array $options [optional] Configuration options. * @return Session */ - private function selectSession($context = SessionPoolInterface::CONTEXT_READ) + public function createSession(array $options = []) { - return $this->sessionPool->session( - $this->instance->name(), - $this->name, - $context - ); + $res = $this->connection->createSession($options + [ + 'database' => GrpcSpannerClient::formatDatabaseName( + $this->projectId, + $this->instance->name(), + $this->name + ) + ]); + + return $this->session($res['name']); } /** - * Convert the simple database name to a fully qualified name. + * Lazily instantiates a session. There are no network requests made at this + * point. To see the operations that can be performed on a session please + * see {@see Google\Cloud\Spanner\Session\Session}. * - * @return string + * Sessions are handled behind the scenes and this method not need to be + * called directly. + * + * @access private + * @param string $name The session's name. + * @return Session */ - private function fullyQualifiedDatabaseName() + public function session($name) { - return GrpcSpannerClient::formatDatabaseName( + return new Session( + $this->connection, $this->projectId, - $this->instance->name(), - $this->name + GrpcSpannerClient::parseInstanceFromSessionName($name), + GrpcSpannerClient::parseDatabaseFromSessionName($name), + GrpcSpannerClient::parseSessionFromSessionName($name) ); } + /** + * Retrieves the database's identity. + * + * @access private + * @return array + */ + public function identity() + { + return [ + 'database' => $this->name, + 'instance' => $this->instance->name(), + ]; + } + /** * Represent the class in a more readable and digestable fashion. * @@ -1119,4 +1274,40 @@ public function __debugInfo() 'sessionPool' => $this->sessionPool, ]; } + + /** + * If no session is already associated with the database use the session + * pool implementation to retrieve a session one - otherwise create on + * demand. + * + * @param string $context [optional] The session context. **Defaults to** + * `r` (READ). + * @return Session + */ + private function selectSession($context = SessionPoolInterface::CONTEXT_READ) + { + if ($this->session) { + return $this->session; + } + + if ($this->sessionPool) { + return $this->session = $this->sessionPool->acquire($context); + } else { + return $this->session = $this->createSession(); + } + } + + /** + * Convert the simple database name to a fully qualified name. + * + * @return string + */ + private function fullyQualifiedDatabaseName() + { + return GrpcSpannerClient::formatDatabaseName( + $this->projectId, + $this->instance->name(), + $this->name + ); + } } diff --git a/src/Spanner/Duration.php b/src/Spanner/Duration.php index c867d55fe99d..4fe506ac0880 100644 --- a/src/Spanner/Duration.php +++ b/src/Spanner/Duration.php @@ -24,7 +24,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient; + * $spanner = new SpannerClient(); * * $seconds = 100; * $nanoSeconds = 000001; diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 44e029983010..30746a269879 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -37,10 +37,9 @@ * * Example: * ``` - * use Google\Cloud\ServiceBuilder; + * use Google\Cloud\Spanner\SpannerClient; * - * $cloud = new ServiceBuilder(); - * $spanner = $cloud->spanner(); + * $spanner = new SpannerClient(); * * $instance = $spanner->instance('my-instance'); * ``` @@ -58,11 +57,6 @@ class Instance */ private $connection; - /** - * @var SessionPool; - */ - private $sessionPool; - /** * @var LongRunningConnectionInterface */ @@ -103,20 +97,18 @@ class Instance * * @param ConnectionInterface $connection The connection to the * Google Cloud Spanner Admin API. - * @param SessionPoolInterface $sessionPool The session pool implementation. * @param LongRunningConnectionInterface $lroConnection An implementation * mapping to methods which handle LRO resolution in the service. * @param array $lroCallables * @param string $projectId The project ID. * @param string $name The instance name. - * @param bool $returnInt64AsObject If true, 64 bit integers will be + * @param bool $returnInt64AsObject [optional] If true, 64 bit integers will be * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit platform * compatibility. **Defaults to** false. * @param array $info [optional] A representation of the instance object. */ public function __construct( ConnectionInterface $connection, - SessionPoolInterface $sessionPool, LongRunningConnectionInterface $lroConnection, array $lroCallables, $projectId, @@ -125,7 +117,6 @@ public function __construct( array $info = [] ) { $this->connection = $connection; - $this->sessionPool = $sessionPool; $this->lroConnection = $lroConnection; $this->lroCallables = $lroCallables; $this->projectId = $projectId; @@ -363,18 +354,24 @@ public function createDatabase($name, array $options = []) * ``` * * @param string $name The database name + * @param array $options [optional] { + * Configuration options. + * + * @type SessionPoolInterface $sessionPool A pool used to manage + * sessions. + * } * @return Database */ - public function database($name) + public function database($name, array $options = []) { return new Database( $this->connection, $this, - $this->sessionPool, $this->lroConnection, $this->lroCallables, $this->projectId, $name, + isset($options['sessionPool']) ? $options['sessionPool'] : null, $this->returnInt64AsObject ); } diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php index ecd45a1f9529..db5a432ba029 100644 --- a/src/Spanner/KeyRange.php +++ b/src/Spanner/KeyRange.php @@ -22,10 +22,9 @@ * * Example: * ``` - * use Google\Cloud\ServiceBuilder; + * use Google\Cloud\Spanner\SpannerClient; * - * $cloud = new ServiceBuilder(); - * $spanner = $cloud->spanner(); + * $spanner = new SpannerClient(); * * // Create a KeyRange for all people named Bob, born in 1969. * $start = $spanner->date(new \DateTime('1969-01-01')); diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index b242972b5ea1..e9bddbf9a88e 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -134,7 +134,7 @@ public function commit(Session $session, array $mutations, array $options = []) } /** - * Rollback a Transaction + * Rollback a Transaction. * * @param Session $session The session to use for the rollback. * Note that the session MUST be the same one in which the @@ -152,12 +152,12 @@ public function rollback(Session $session, $transactionId, array $options = []) } /** - * Run a query + * Run a query. * * @param Session $session The session to use to execute the SQL. * @param string $sql The query string. * @param array $options [optional] Configuration options. - * @return array + * @return Result */ public function execute(Session $session, $sql, array $options = []) { @@ -171,12 +171,18 @@ public function execute(Session $session, $sql, array $options = []) $context = $this->pluck('transactionContext', $options); - $res = $this->connection->executeSql([ - 'sql' => $sql, - 'session' => $session->name() - ] + $options); + $call = function ($resumeToken = null) use ($session, $sql, $options) { + if ($resumeToken) { + $options['resumeToken'] = $resumeToken; + } + + return $this->connection->executeStreamingSql([ + 'sql' => $sql, + 'session' => $session->name() + ] + $options); + }; - return $this->createResult($session, $res, $context); + return new Result($this, $session, $call, $context, $this->mapper); } /** @@ -205,22 +211,26 @@ public function read(Session $session, $table, KeySet $keySet, array $columns, a ]; $context = $this->pluck('transactionContext', $options); - $res = $this->connection->read([ - 'table' => $table, - 'session' => $session->name(), - 'columns' => $columns, - 'keySet' => $this->flattenKeySet($keySet) - ] + $options); - return $this->createResult($session, $res, $context); + $call = function ($resumeToken = null) use ($table, $session, $columns, $keySet, $options) { + if ($resumeToken) { + $options['resumeToken'] = $resumeToken; + } + + return $this->connection->streamingRead([ + 'table' => $table, + 'session' => $session->name(), + 'columns' => $columns, + 'keySet' => $this->flattenKeySet($keySet) + ] + $options); + }; + + return new Result($this, $session, $call, $context, $this->mapper); } /** * Create a read/write transaction. * - * @todo if a transaction is already available on the session, get it instead - * of starting a new one? - * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest * * @param Session $session The session to start the transaction in. @@ -249,26 +259,6 @@ public function snapshot(Session $session, array $options = []) return $this->createSnapshot($session, $res); } - /** - * Execute a service call to begin a transaction or snapshot. - * - * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest - * - * @param Session $session The session to start the snapshot in. - * @param array $options [optional] Configuration options. - * @return array - */ - private function beginTransaction(Session $session, array $options = []) - { - $options += [ - 'transactionOptions' => [] - ]; - - return $this->connection->beginTransaction($options + [ - 'session' => $session->name(), - ]); - } - /** * Create a Transaction instance from a response object. * @@ -276,7 +266,7 @@ private function beginTransaction(Session $session, array $options = []) * @param array $res The transaction response. * @return Transaction */ - private function createTransaction(Session $session, array $res) + public function createTransaction(Session $session, array $res) { return new Transaction($this, $session, $res['id']); } @@ -288,7 +278,7 @@ private function createTransaction(Session $session, array $res) * @param array $res The snapshot response. * @return Snapshot */ - private function createSnapshot(Session $session, array $res) + public function createSnapshot(Session $session, array $res) { $timestamp = null; if (isset($res['readTimestamp'])) { @@ -299,38 +289,23 @@ private function createSnapshot(Session $session, array $res) } /** - * Transform a service read or executeSql response to a friendly result. + * Execute a service call to begin a transaction or snapshot. * - * @codingStandardsIgnoreStart - * @param Session $session The current session. - * @param array $res [ResultSet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet) - * @param string $transactionContext - * @return Result - * @codingStandardsIgnoreEnd + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the snapshot in. + * @param array $options [optional] Configuration options. + * @return array */ - private function createResult(Session $session, array $res, $transactionContext) + private function beginTransaction(Session $session, array $options = []) { - $columns = isset($res['metadata']['rowType']['fields']) - ? $res['metadata']['rowType']['fields'] - : []; - - $rows = []; - if (isset($res['rows'])) { - foreach ($res['rows'] as $row) { - $rows[] = $this->mapper->decodeValues($columns, $row); - } - } - - $options = []; - if (isset($res['metadata']['transaction']['id'])) { - if ($transactionContext === SessionPoolInterface::CONTEXT_READ) { - $options['snapshot'] = $this->createSnapshot($session, $res['metadata']['transaction']); - } else { - $options['transaction'] = $this->createTransaction($session, $res['metadata']['transaction']); - } - } + $options += [ + 'transactionOptions' => [] + ]; - return new Result($res, $rows, $options); + return $this->connection->beginTransaction($options + [ + 'session' => $session->name(), + ]); } /** diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index c168438ce963..40b1d09309f7 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -17,17 +17,20 @@ namespace Google\Cloud\Spanner; +use Google\Cloud\Core\Exception\ServiceException; +use Google\Cloud\Spanner\Session\Session; +use Google\Cloud\Spanner\Session\SessionPoolInterface; + /** * Represent a Google Cloud Spanner lookup result (either read or executeSql). * * Example: * ``` - * use Google\Cloud\ServiceBuilder; + * use Google\Cloud\Spanner\SpannerClient; * - * $cloud = new ServiceBuilder(); - * $spanner = $cloud->spanner(); - * $database = $spanner->connect('my-instance', 'my-database'); + * $spanner = new SpannerClient(); * + * $database = $spanner->connect('my-instance', 'my-database'); * $result = $database->execute('SELECT * FROM Posts'); * ``` * @@ -36,88 +39,138 @@ class Result implements \IteratorAggregate { /** - * @var array + * @var array|null */ - private $result; + private $cachedValues; /** * @var array */ - private $rows; + private $columns = []; /** - * @var array + * @var ValueMapper */ - private $options; + private $mapper; /** - * @param array $result The query or read result. - * @param array $rows The rows, formatted and decoded. - * @param array $options Additional result options and info. + * @var array|null */ - public function __construct(array $result, array $rows, array $options = []) - { - $this->result = $result; - $this->rows = $rows; - $this->options = $options; - } + private $metadata; /** - * Return result metadata - * - * Example: - * ``` - * $metadata = $result->metadata(); - * ``` - * - * @codingStandardsIgnoreStart - * @return array [ResultSetMetadata](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetMetadata). - * @codingStandardsIgnoreEnd + * @var Operation */ - public function metadata() - { - return $this->result['metadata']; + private $operation; + + /** + * @var string|null + */ + private $resumeToken; + + /** + * @var Session + */ + private $session; + + /** + * @var Snapshot|null + */ + private $snapshot; + + /** + * @var array|null + */ + private $stats; + + /** + * @var Transaction|null + */ + private $transaction; + + /** + * @var string + */ + private $transactionContext; + + /** + * @param Operation $operation Runs operations against Google Cloud Spanner. + * @param Session $session The session used for any operations executed. + * @param \Generator $resultGenerator Reads rows from Google Cloud Spanner. + * @param string $transactionContext The transaction's context. + * @param ValueMapper $mapper Maps values. + */ + public function __construct( + Operation $operation, + Session $session, + callable $call, + $transactionContext, + ValueMapper $mapper + ) { + $this->operation = $operation; + $this->session = $session; + $this->call = $call; + $this->transactionContext = $transactionContext; + $this->mapper = $mapper; } /** * Return the formatted and decoded rows. * + * If the stream is interrupted an attempt will be made to resume. + * * Example: * ``` * $rows = $result->rows(); * ``` * - * @return array|null + * @return \Generator */ public function rows() { - return $this->rows; + $call = $this->call; + + try { + foreach ($this->getRows($call()) as $row) { + yield $row; + } + } catch (ServiceException $ex) { + if (!$this->resumeToken) { + throw $ex; + } + + // If we have a token, attempt to resume + foreach ($this->getRows($call($this->resumeToken)) as $row) { + yield $row; + } + } } /** - * Return the first row, or null. + * Return result metadata. * - * Useful when selecting a single row. + * Will be populated once the result set is iterated upon. * * Example: * ``` - * $row = $result->firstRow(); + * $metadata = $result->metadata(); * ``` * - * @return array|null + * @codingStandardsIgnoreStart + * @return array|null [ResultSetMetadata](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetMetadata). + * @codingStandardsIgnoreEnd */ - public function firstRow() + public function metadata() { - return (isset($this->rows[0])) - ? $this->rows[0] - : null; + return $this->metadata; } /** * Get the query plan and execution statistics for the query that produced * this result set. * - * Stats are not returned by default. + * Stats are not returned by default and will not be accessible until the + * entire set of results has been iterated through. * * Example: * ``` @@ -137,67 +190,137 @@ public function firstRow() */ public function stats() { - return (isset($this->result['stats'])) - ? $this->result['stats'] - : null; + return $this->stats; } /** - * Returns a transaction which was begun in the read or execute, if one exists. + * Returns a snapshot which was begun in the read or execute, if one exists. + * + * Will be populated once the result set is iterated upon. * * Example: * ``` - * $transaction = $result->transaction(); + * $snapshot = $result->snapshot(); * ``` * - * @return Transaction|null + * @return Snapshot|null */ - public function transaction() + public function snapshot() { - return (isset($this->options['transaction'])) - ? $this->options['transaction'] - : null; + return $this->snapshot; } /** - * Returns a snapshot which was begun in the read or execute, if one exists. + * Returns a transaction which was begun in the read or execute, if one exists. + * + * Will be populated once the result set is iterated upon. * * Example: * ``` - * $snapshot = $result->snapshot(); + * $transaction = $result->transaction(); * ``` * - * @return Snapshot|null + * @return Transaction|null */ - public function snapshot() + public function transaction() { - return (isset($this->options['snapshot'])) - ? $this->options['snapshot'] - : null; + return $this->transaction; } /** - * Get the entire query or read response as given by the API. + * @access private + */ + public function getIterator() + { + return $this->rows(); + } + + /** + * Yields rows from a partial result set. * - * Example: - * ``` - * $info = $result->info(); - * ``` + * @return \Generator + */ + private function getRowsFromPartial(array $partial) + { + $this->stats = isset($partial['stats']) ? $partial['stats'] : null; + $this->resumeToken = isset($partial['resumeToken']) ? $partial['resumeToken'] : null; + + if (isset($partial['metadata'])) { + $this->metadata = $partial['metadata']; + $this->columns = $partial['metadata']['rowType']['fields']; + } + + if (isset($partial['metadata']['transaction']['id'])) { + if ($this->transactionContext === SessionPoolInterface::CONTEXT_READ) { + $this->snapshot = $this->operation->createSnapshot( + $this->session, + $partial['metadata']['transaction'] + ); + } else { + $this->transaction = $this->operation->createTransaction( + $this->session, + $partial['metadata']['transaction'] + ); + } + } + + if ($this->cachedValues) { + $partial['values'] = $this->mergeValues($this->cachedValues, $partial['values']); + $this->cachedValues = null; + } + + if (isset($partial['chunkedValue'])) { + $this->cachedValues = $partial['values']; + return; + } + + $rows = []; + $columnCount = count($this->columns); + + if ($columnCount > 0 && isset($partial['values'])) { + $rows = array_chunk($partial['values'], $columnCount); + } + + foreach ($rows as $row) { + yield $this->mapper->decodeValues($this->columns, $row); + } + } + + /** + * Merge result set values together. * - * @codingStandardsIgnoreStart - * @return array [ResultSet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet). - * @codingStandardsIgnoreEnd + * @param array $cached + * @param array $new + * @return mixed */ - public function info() + private function mergeValues(array $cached, array $new) { - return $this->result; + $lastCachedItem = array_pop($cached); + $firstNewItem = array_shift($new); + $item = $firstNewItem; + + if (is_string($lastCachedItem) && is_string($firstNewItem)) { + $item = $lastCachedItem . $firstNewItem; + } elseif (is_array($lastCachedItem)) { + $item = $this->mergeValues($lastCachedItem, $firstNewItem); + } else { + array_push($cached, $lastCachedItem); + } + + array_push($cached, $item); + return array_merge($cached, $new); } /** - * @access private + * @param \Generator $results + * @return \Generator */ - public function getIterator() + private function getRows(\Generator $results) { - return new \ArrayIterator($this->rows); + foreach ($results as $partial) { + foreach ($this->getRowsFromPartial($partial) as $row) { + yield $row; + } + } } } diff --git a/src/Spanner/Session/CacheSessionPool.php b/src/Spanner/Session/CacheSessionPool.php new file mode 100644 index 000000000000..da881bc7860b --- /dev/null +++ b/src/Spanner/Session/CacheSessionPool.php @@ -0,0 +1,547 @@ +connect('my-instance', 'my-database', [ + * 'sessionPool' => $sessionPool + * ]); + * ``` + */ +class CacheSessionPool implements SessionPoolInterface +{ + const CACHE_KEY_TEMPLATE = 'cache-session-pool.%s.%s'; + + /** + * @var array + */ + private static $defaultConfig = [ + 'maxSessions' => PHP_INT_MAX, + 'minSessions' => 1, + 'shouldWaitForSession' => true, + 'maxCyclesToWaitForSession' => 30, + 'sleepIntervalSeconds' => .5 + ]; + + /** + * @var CacheItemPoolInterface + */ + private $cacheItemPool; + + /** + * @var string + */ + private $cacheKey; + + /** + * @var array + */ + private $config; + + /** + * @var Database + */ + private $database; + + /** + * @param CacheItemPoolInterface $cacheItemPool A PSR-6 compatible cache + * implementation used to store the session data. + * @param array $config [optional] { + * Configuration Options. + * + * @type int $maxSessions The maximum number of sessions to store in the + * pool. **Defaults to** PHP_INT_MAX. + * @type int $minSessions The minimum number of sessions to store in the + * pool. **Defaults to** `1`. + * @type bool $shouldWaitForSession If the pool is full, whether to block + * until a new session is available. **Defaults to* `true`. + * @type int $maxCyclesToWaitForSession The maximum number cycles to + * wait for a session before throwing an exception. **Defaults to** + * `30`. Ignored when $shouldWaitForSession is `false`. + * @type float $sleepIntervalSeconds The sleep interval between cycles. + * **Defaults to** `0.5`. Ignored when $shouldWaitForSession is + * `false`. + * @type LockInterface $lock A lock implementation capable of blocking. + * **Defaults to** an flock based implementation. + * } + * @throws \InvalidArgumentException + */ + public function __construct(CacheItemPoolInterface $cacheItemPool, array $config = []) + { + $this->cacheItemPool = $cacheItemPool; + $this->config = $config + self::$defaultConfig; + + if (!isset($this->config['lock'])) { + $this->config['lock'] = $this->getDefaultLock(); + } + + $this->validateConfig(); + } + + /** + * Acquire a session from the pool. + * + * @param string $context The type of session to fetch. May be either `r` + * (READ) or `rw` (READ/WRITE). **Defaults to** `r`. + * @return Session + * @throws \RuntimeException + */ + public function acquire($context = SessionPoolInterface::CONTEXT_READ) + { + // Try to get a session, run maintenance on the pool, and calculate if + // we need to create any new sessions. + list($session, $toCreate) = $this->config['lock']->synchronize(function () { + $toCreate = []; + $session = null; + $shouldSave = false; + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = (array) $item->get() ?: $this->initialize(); + + // If the queue has items in it, let's shift one off, however if the + // queue is empty and we have maxed out the number of sessions let's + // attempt to purge any orphaned items from the pool to make room + // for more. + if ($data['queue']) { + $session = $this->getSession($data); + $shouldSave = true; + } elseif ($this->config['maxSessions'] <= $this->getSessionCount($data)) { + $this->purgeOrphanedInUseSessions($data); + $this->purgeOrphanedToCreateItems($data); + $shouldSave = true; + } + + $toCreate = $this->buildToCreateList($data, is_array($session)); + $data['toCreate'] += $toCreate; + + if ($shouldSave || $toCreate) { + $this->cacheItemPool->save($item->set($data)); + } + + return [$session, $toCreate]; + }); + + // Create sessions if needed. + if ($toCreate) { + $createdSessions = []; + $exception = null; + + try { + $createdSessions = $this->createSessions(count($toCreate)); + } catch (\Exception $ex) { + $exception = $ex; + } + + $session = $this->config['lock']->synchronize(function () use ( + $session, + $toCreate, + $createdSessions, + $exception + ) { + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = $item->get(); + $data['queue'] = array_merge($data['queue'], $createdSessions); + + // Now that we've created the sessions, we can remove them from + // the list of intent. + foreach ($toCreate as $id => $time) { + unset($data['toCreate'][$id]); + } + + if (!$session && !$exception) { + $session = array_shift($data['queue']); + + $data['inUse'][$session['name']] = $session + [ + 'lastActive' => $this->time() + ]; + } + + $this->cacheItemPool->save($item->set($data)); + + return $session; + }); + + if ($exception) { + throw $exception; + } + } + + if ($session) { + $session = $this->handleSession($session); + } + + // If we don't have a session, let's wait for one or throw an exception. + if (!$session) { + if (!$this->config['shouldWaitForSession']) { + throw new \RuntimeException('No sessions available.'); + } + + $session = $this->waitForNextAvailableSession(); + } + + return $this->database->session($session['name']); + } + + /** + * Release a session back to the pool. + * + * @param Session $session The session. + */ + public function release(Session $session) + { + $this->config['lock']->synchronize(function () use ($session) { + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = $item->get(); + + unset($data['inUse'][$session->name()]); + array_push($data['queue'], [ + 'name' => $session->name(), + 'expiration' => $session->expiration() ?: $this->time() + SessionPoolInterface::SESSION_EXPIRATION_SECONDS + ]); + $this->cacheItemPool->save($item->set($data)); + }); + } + + /** + * Clear the session pool. + */ + public function clear() + { + $this->cacheItemPool->clear(); + } + + /** + * Set the database used to make calls to manage sessions. + * + * @param Database $database The database. + */ + public function setDatabase(Database $database) + { + $this->database = $database; + $identity = $database->identity(); + $this->cacheKey = sprintf(self::CACHE_KEY_TEMPLATE, $identity['instance'], $identity['database']); + } + + /** + * Get the underlying cache implementation. + * + * @return CacheItemPoolInterface + */ + public function cacheItemPool() + { + return $this->cacheItemPool; + } + + /** + * Get the current unix timestamp. + * + * @return int + */ + protected function time() + { + return time(); + } + + /** + * Builds out a list of timestamps indicating the start time of the intent + * to create a session. + * + * @param array $data + * @param bool $hasSession + * @return array + */ + private function buildToCreateList(array $data, $hasSession) + { + $number = 0; + $toCreate = []; + $time = $this->time(); + $count = $this->getSessionCount($data); + + if ($count < $this->config['minSessions']) { + $number = $this->config['minSessions'] - $count; + } elseif (!$hasSession && !$data['queue'] && $count < $this->config['maxSessions']) { + $number++; + } + + for ($i = 0; $i < $number; $i++) { + $toCreate[uniqid($time . '_')] = $time; + } + + return $toCreate; + } + + /** + * Purge any items in the to create queue that have been inactive for 20 + * minutes or more. + * + * @param array $data + */ + private function purgeOrphanedToCreateItems(array &$data) + { + foreach ($data['toCreate'] as $key => $timestamp) { + if ($timestamp + 1200 < $this->time()) { + unset($data['toCreate'][$key]); + } + } + } + + /** + * Purge any in use sessions that have been inactive for 20 minutes or more. + * + * @param array $data + */ + private function purgeOrphanedInUseSessions(array &$data) + { + foreach ($data['inUse'] as $key => $session) { + if ($session['lastActive'] + 1200 < $this->time()) { + unset($data['inUse'][$key]); + } + } + } + + /** + * Initialize the session data. + * + * @return array + */ + private function initialize() + { + return [ + 'queue' => [], + 'inUse' => [], + 'toCreate' => [] + ]; + } + + /** + * Returns the total count of sessions in queue, use, and in the process of + * being created. + * + * @param array $data + * @return int + */ + private function getSessionCount(array $data) + { + $count = 0; + + foreach ($data as $sessionType) { + $count += count($sessionType); + } + + return $count; + } + + /** + * Gets the next session in the queue, clearing out which are expired. + * + * @param array $data + * @return array|null + */ + private function getSession(array &$data) + { + $session = array_shift($data['queue']); + + if ($session) { + if ($session['expiration'] - 60 < $this->time()) { + return $this->getSession($data); + } + + $data['inUse'][$session['name']] = $session + [ + 'lastActive' => $this->time() + ]; + } + + return $session; + } + + /** + * Creates sessions up to the count provided. + * + * @param int $count + * @return array + */ + private function createSessions($count) + { + $sessions = []; + + for ($i = 0; $i < $count; $i++) { + $sessions[] = [ + 'name' => $this->database->createSession()->name(), + 'expiration' => $this->time() + SessionPoolInterface::SESSION_EXPIRATION_SECONDS + ]; + } + + return $sessions; + } + + /** + * If necessary, triggers a network request to determine the status of the + * provided session. + * + * @param array $session + * @return bool + */ + private function isSessionValid(array $session) + { + $halfHourBeforeExpiration = $session['expiration'] - (SessionPoolInterface::SESSION_EXPIRATION_SECONDS / 2); + + if ($this->time() < $halfHourBeforeExpiration) { + return true; + } elseif ($halfHourBeforeExpiration < $this->time() && $this->time() < $session['expiration']) { + return $this->database + ->session($session['name']) + ->exists(); + } + + return false; + } + + /** + * If the session is valid, return it - otherwise remove from the in use + * list. + * + * @param array $session + * @return array|null + */ + private function handleSession(array $session) + { + if ($this->isSessionValid($session)) { + return $session; + } + + $this->config['lock']->synchronize(function () use ($session) { + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = $item->get(); + unset($data['inUse'][$session['name']]); + $this->cacheItemPool->save($item->set($data)); + }); + } + + /** + * Blocks until a session becomes available. + * + * @return array + * @throws \RuntimeException + */ + private function waitForNextAvailableSession() + { + $elapsedCycles = 0; + + while (true) { + $session = $this->config['lock']->synchronize(function () use ($elapsedCycles) { + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = $item->get(); + $session = $this->getSession($data); + + if ($session) { + $this->cacheItemPool->save($item->set($data)); + return $session; + } + + if ($this->config['maxCyclesToWaitForSession'] <= $elapsedCycles) { + $this->cacheItemPool->save($item->set($data)); + + throw new \RuntimeException( + 'A session did not become available in the allotted number of attempts.' + ); + } + }); + + if ($session && $this->handleSession($session)) { + return $session; + } + + $elapsedCycles++; + usleep($this->config['sleepIntervalSeconds'] * 1000000); + } + } + + /** + * Get the default lock. + * + * @return LockInterface + */ + private function getDefaultLock() + { + $store = new FlockStore(sys_get_temp_dir()); + + return new SymfonyLockAdapter( + (new Factory($store))->createLock($this->cacheKey) + ); + } + + /** + * Validate the config. + * + * @param array $config + * @throws \InvalidArgumentException + */ + private function validateConfig() + { + $mustBePositiveKeys = ['maxCyclesToWaitForSession', 'maxSessions', 'minSessions', 'sleepIntervalSeconds']; + + foreach ($mustBePositiveKeys as $key) { + if ($this->config[$key] < 0) { + throw new \InvalidArgumentException("$key may not be negative"); + } + } + + if ($this->config['maxSessions'] < $this->config['minSessions']) { + throw new \InvalidArgumentException('minSessions cannot exceed maxSessions'); + } + + if (!$this->config['lock'] instanceof LockInterface) { + throw new \InvalidArgumentException( + 'The lock must implement Google\Cloud\Core\Lock\LockInterface' + ); + } + } +} diff --git a/src/Spanner/Session/Session.php b/src/Spanner/Session/Session.php index 0ecf8f3d3a00..c1ae06042f4a 100644 --- a/src/Spanner/Session/Session.php +++ b/src/Spanner/Session/Session.php @@ -51,6 +51,11 @@ class Session */ private $name; + /** + * @var int|null + */ + private $expiration; + /** * @param ConnectionInterface $connection A connection to Cloud Spanner. * @param string $projectId The project ID. @@ -58,8 +63,13 @@ class Session * @param string $database The database name. * @param string $name The session name. */ - public function __construct(ConnectionInterface $connection, $projectId, $instance, $database, $name) - { + public function __construct( + ConnectionInterface $connection, + $projectId, + $instance, + $database, + $name + ) { $this->connection = $connection; $this->projectId = $projectId; $this->instance = $instance; @@ -68,7 +78,7 @@ public function __construct(ConnectionInterface $connection, $projectId, $instan } /** - * Return info on the session + * Return info on the session. * * @return array An array containing the `projectId`, `instance`, `database` and session `name` keys. */ @@ -130,7 +140,33 @@ public function name() } /** + * Sets the expiration. + * + * @param int $expiration [optional] The Unix timestamp in seconds upon + * which the session will expire. **Defaults to** now plus 60 + * minutes. + * @return int + */ + public function setExpiration($expiration = null) + { + $this->expiration = $expiration ?: time() + SessionPoolInterface::SESSION_EXPIRATION_SECONDS; + } + + /** + * Gets the expiration. + * + * @return int|null + */ + public function expiration() + { + return $this->expiration; + } + + /** + * Represent the class in a more readable and digestable fashion. + * * @access private + * @codeCoverageIgnore */ public function __debugInfo() { diff --git a/src/Spanner/Session/SessionClient.php b/src/Spanner/Session/SessionClient.php deleted file mode 100644 index 1642e9519c3a..000000000000 --- a/src/Spanner/Session/SessionClient.php +++ /dev/null @@ -1,104 +0,0 @@ -connection = $connection; - $this->projectId = $projectId; - } - - /** - * Create a new session in the given instance and database. - * - * @param string $instance The simple instance name. - * @param string $database The simple database name. - * @param array $options [optional] Configuration options. - * @return Session|null If the operation succeeded, a Session object will be returned, - * otherwise null. - */ - public function create($instance, $database, array $options = []) - { - $res = $this->connection->createSession($options + [ - 'database' => SpannerClient::formatDatabaseName($this->projectId, $instance, $database) - ]); - - $session = null; - if (isset($res['name'])) { - $session = $this->session($res['name']); - } - - return $session; - } - - /** - * Get a Session - * - * @param string $sessionName The Session name. - * @return Session - */ - public function session($sessionName) - { - return new Session( - $this->connection, - $this->projectId, - SpannerClient::parseInstanceFromSessionName($sessionName), - SpannerClient::parseDatabaseFromSessionName($sessionName), - SpannerClient::parseSessionFromSessionName($sessionName) - ); - } - - /** - * @access private - */ - public function __debugInfo() - { - return [ - 'connection' => get_class($this->connection), - 'projectId' => $this->projectId - ]; - } -} diff --git a/src/Spanner/Session/SessionPool.php b/src/Spanner/Session/SessionPool.php deleted file mode 100644 index 2c96d8b12e88..000000000000 --- a/src/Spanner/Session/SessionPool.php +++ /dev/null @@ -1,63 +0,0 @@ -sessionClient = $sessionClient; - } - - /** - * @access private - */ - public function addSession(Session $session) - { - $this->sessions[] = $sessions; - } - - /** - * @access private - */ - public function session($instance, $database, $context, array $options = []) - { - return array_rand($this->sessions); - } - - /** - * @access private - */ - public function refreshSessions() - { - // send a request from each session to keep it alive. - } -} diff --git a/src/Spanner/Session/SessionPoolInterface.php b/src/Spanner/Session/SessionPoolInterface.php index 2aa4943d981c..5ef776a9bc32 100644 --- a/src/Spanner/Session/SessionPoolInterface.php +++ b/src/Spanner/Session/SessionPoolInterface.php @@ -17,6 +17,8 @@ namespace Google\Cloud\Spanner\Session; +use Google\Cloud\Spanner\Database; + /** * Describes a session pool. */ @@ -24,9 +26,34 @@ interface SessionPoolInterface { const CONTEXT_READ = 'r'; const CONTEXT_READWRITE = 'rw'; + const SESSION_EXPIRATION_SECONDS = 3600; + + /** + * Acquire a session from the pool. + * + * @param string $context The type of session to fetch. May be either `r` + * (READ) or `rw` (READ/WRITE). **Defaults to** `r`. + * @return Session + * @throws \RunTimeException + */ + public function acquire($context); + + /** + * Release a session back to the pool. + * + * @param Session $session + */ + public function release(Session $session); + + /** + * Clear the session pool. + */ + public function clear(); /** - * Get a session from the pool + * Set the database used to make calls to manage sessions. + * + * @param Database $database */ - public function session($instance, $database, $context, array $options = []); + public function setDatabase(Database $database); } diff --git a/src/Spanner/Session/SimpleSessionPool.php b/src/Spanner/Session/SimpleSessionPool.php deleted file mode 100644 index d1e756f7b9cf..000000000000 --- a/src/Spanner/Session/SimpleSessionPool.php +++ /dev/null @@ -1,54 +0,0 @@ -sessionClient = $sessionClient; - } - - /** - * @access private - */ - public function session($instance, $database, $mode, array $options = []) - { - if (!isset($this->sessions[$instance.$database.$mode])) { - $this->sessions[$instance.$database] = $this->sessionClient->create($instance, $database, $options); - } - - return $this->sessions[$instance.$database]; - } -} diff --git a/src/Spanner/Snapshot.php b/src/Spanner/Snapshot.php index 09fd1e2e5af3..8655e7d5349e 100644 --- a/src/Spanner/Snapshot.php +++ b/src/Spanner/Snapshot.php @@ -49,6 +49,10 @@ * ] * ] * ); + * + * $firstRow = $result + * ->rows() + * ->current(); * ``` * * @param string $sql The query string to execute. @@ -73,7 +77,10 @@ * $columns = ['ID', 'title', 'content']; * * $result = $snapshot->read('Posts', $keySet, $columns); - * ``` + * + * $firstRow = $result + * ->rows() + * ->current(); * * @param string $table The table name. * @param KeySet $keySet The KeySet to select rows. diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 3c4034bfac67..c28c4ef0db68 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -30,8 +30,6 @@ use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\Grpc; use Google\Cloud\Spanner\Connection\LongRunningConnection; -use Google\Cloud\Spanner\Session\SessionClient; -use Google\Cloud\Spanner\Session\SimpleSessionPool; use google\spanner\admin\instance\v1\Instance\State; use Psr\Http\StreamInterface; @@ -70,16 +68,6 @@ class SpannerClient */ private $lroConnection; - /** - * @var SessionClient - */ - protected $sessionClient; - - /** - * @var SessionPool - */ - protected $sessionPool; - /** * @var bool */ @@ -109,7 +97,7 @@ class SpannerClient * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit * platform compatibility. **Defaults to** false. * } - * @throws Google\Cloud\Exception\GoogleException + * @throws Google\Cloud\Core\Exception\GoogleException */ public function __construct(array $config = []) { @@ -123,12 +111,7 @@ public function __construct(array $config = []) $this->connection = new Grpc($this->configureAuthentication($config)); $this->lroConnection = new LongRunningConnection($this->connection); - - $this->sessionClient = new SessionClient($this->connection, $this->projectId); - $this->sessionPool = new SimpleSessionPool($this->sessionClient); - $this->returnInt64AsObject = $config['returnInt64AsObject']; - $this->lroCallables = [ [ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', @@ -287,7 +270,6 @@ public function instance($name, array $instance = []) { return new Instance( $this->connection, - $this->sessionPool, $this->lroConnection, $this->lroCallables, $this->projectId, @@ -356,15 +338,21 @@ function (array $instance) { * * @param Instance|string $instance The instance object or instance name. * @param string $name The database name. + * @param array $options [optional] { + * Configuration options. + * + * @type SessionPoolInterface $sessionPool A pool used to manage + * sessions. + * } * @return Database */ - public function connect($instance, $name) + public function connect($instance, $name, array $options = []) { if (is_string($instance)) { $instance = $this->instance($instance); } - $database = $instance->database($name); + $database = $instance->database($name, $options); return $database; } @@ -517,21 +505,6 @@ public function duration($seconds, $nanos = 0) return new Duration($seconds, $nanos); } - /** - * Get the session client - * - * Example: - * ``` - * $sessionClient = $spanner->sessionClient(); - * ``` - * - * @return SessionClient - */ - public function sessionClient() - { - return $this->sessionClient; - } - /** * Resume a Long Running Operation * diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 957bf91daf6e..e9c124324e38 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -44,10 +44,9 @@ * * Example: * ``` - * use Google\Cloud\ServiceBuilder; + * use Google\Cloud\Spanner\SpannerClient; * - * $cloud = new ServiceBuilder(); - * $spanner = $cloud->spanner(); + * $spanner = new SpannerClient(); * * $database = $spanner->connect('my-instance', 'my-database'); * @@ -76,6 +75,10 @@ * ] * ] * ); + * + * $firstRow = $result + * ->rows() + * ->current(); * ``` * * @param string $sql The query string to execute. @@ -100,6 +103,10 @@ * $columns = ['ID', 'title', 'content']; * * $result = $transaction->read('Posts', $keySet, $columns); + * + * $firstRow = $result + * ->rows() + * ->current(); * ``` * * @param string $table The table name. @@ -444,6 +451,11 @@ public function state() return $this->state; } + public function id() + { + return $this->transactionId; + } + /** * Format, validate and enqueue mutations in the transaction. * diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index e6c48ff90043..39c6620e07d0 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -97,11 +97,11 @@ public function encodeValuesAsSimpleType(array $values) * Accepts a list of columns (with name and type) and a row from read or * executeSql and decodes each value to its corresponding PHP type. * - * @param array $columns The list of columns + * @param array $columns The list of columns. * @param array $row The row data. * @return array The decoded row data. */ - public function decodeValues(array $columns, array $row, $extractResult = false) + public function decodeValues(array $columns, array $row) { $cols = []; $types = []; @@ -147,6 +147,10 @@ public function createTimestampWithNanos($timestamp) */ private function decodeValue($value, array $type) { + if ($value === null || $value === '') { + return $value; + } + switch ($type['code']) { case self::TYPE_INT64: $value = $this->returnInt64AsObject @@ -176,7 +180,7 @@ private function decodeValue($value, array $type) break; case self::TYPE_STRUCT: - $value = $this->decodeValues($type['structType']['fields'], $value, true); + $value = $this->decodeValues($type['structType']['fields'], $value); break; case self::TYPE_FLOAT64: diff --git a/tests/snippets/Spanner/DatabaseTest.php b/tests/snippets/Spanner/DatabaseTest.php index e7eb9b50b20b..36d85e57b480 100644 --- a/tests/snippets/Spanner/DatabaseTest.php +++ b/tests/snippets/Spanner/DatabaseTest.php @@ -55,18 +55,20 @@ public function setUp() $session = $this->prophesize(Session::class); $sessionPool = $this->prophesize(SessionPoolInterface::class); - $sessionPool->session(Argument::any(), Argument::any(), Argument::any()) + $sessionPool->acquire(Argument::any()) ->willReturn($session->reveal()); + $sessionPool->setDatabase(Argument::any()) + ->willReturn(null); $this->connection = $this->prophesize(ConnectionInterface::class); $this->database = \Google\Cloud\Dev\stub(Database::class, [ $this->connection->reveal(), $instance->reveal(), - $sessionPool->reveal(), $this->prophesize(LongRunningConnectionInterface::class)->reveal(), [], self::PROJECT, - self::DATABASE + self::DATABASE, + $sessionPool->reveal() ], ['connection', 'operation']); } @@ -246,9 +248,9 @@ public function testRunTransaction() $this->connection->rollback(Argument::any()) ->shouldNotBeCalled(); - $this->connection->executeSql(Argument::any()) + $this->connection->executeStreamingSql(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -261,12 +263,8 @@ public function testRunTransaction() ] ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'values' => [0] + ])); $this->stubOperation(); @@ -293,14 +291,16 @@ public function testRunTransactionRollback() $this->connection->rollback(Argument::any()) ->shouldBeCalled(); - $this->connection->executeSql(Argument::any()) + $this->connection->executeStreamingSql(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ - 'rowType' => [] + 'rowType' => [ + 'fields' => [] + ] ], - 'rows' => [] - ]); + 'values' => [] + ])); $this->stubOperation(); @@ -485,9 +485,9 @@ public function testDelete() public function testExecute() { - $this->connection->executeSql(Argument::any()) + $this->connection->executeStreamingSql(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -500,12 +500,8 @@ public function testExecute() ] ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'values' => [0] + ])); $this->stubOperation(); @@ -518,9 +514,9 @@ public function testExecute() public function testExecuteBeginSnapshot() { - $this->connection->executeSql(Argument::any()) + $this->connection->executeStreamingSql(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -536,12 +532,8 @@ public function testExecuteBeginSnapshot() 'id' => self::TRANSACTION ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'values' => [0] + ])); $this->stubOperation(); @@ -555,9 +547,9 @@ public function testExecuteBeginSnapshot() public function testExecuteBeginTransaction() { - $this->connection->executeSql(Argument::any()) + $this->connection->executeStreamingSql(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -573,12 +565,8 @@ public function testExecuteBeginTransaction() 'id' => self::TRANSACTION ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'values' => [0] + ])); $this->stubOperation(); @@ -593,9 +581,9 @@ public function testExecuteBeginTransaction() public function testRead() { - $this->connection->read(Argument::any()) + $this->connection->streamingRead(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -608,12 +596,8 @@ public function testRead() ] ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'rows' => [0] + ])); $this->stubOperation(); @@ -627,9 +611,9 @@ public function testRead() public function testReadWithSnapshot() { - $this->connection->read(Argument::any()) + $this->connection->streamingRead(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -645,12 +629,8 @@ public function testReadWithSnapshot() 'id' => self::TRANSACTION ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'values' => [0] + ])); $this->stubOperation(); @@ -665,9 +645,9 @@ public function testReadWithSnapshot() public function testReadWithTransaction() { - $this->connection->read(Argument::any()) + $this->connection->streamingRead(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -683,12 +663,8 @@ public function testReadWithTransaction() 'id' => self::TRANSACTION ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'values' => [0] + ])); $this->stubOperation(); @@ -702,6 +678,23 @@ public function testReadWithTransaction() $this->assertInstanceOf(Transaction::class, $res->returnVal()->transaction()); } + public function testSessionPool() + { + $snippet = $this->snippetFromMethod(Database::class, 'sessionPool'); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke('pool'); + $this->assertInstanceOf(SessionPoolInterface::class, $res->returnVal()); + } + + public function testClose() + { + $snippet = $this->snippetFromMethod(Database::class, 'close'); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke(); + } + public function testIam() { $snippet = $this->snippetFromMethod(Database::class, 'iam'); @@ -710,4 +703,9 @@ public function testIam() $res = $snippet->invoke('iam'); $this->assertInstanceOf(Iam::class, $res->returnVal()); } + + private function resultGenerator(array $data) + { + yield $data; + } } diff --git a/tests/snippets/Spanner/InstanceTest.php b/tests/snippets/Spanner/InstanceTest.php index b5bb836df759..e30f2975c992 100644 --- a/tests/snippets/Spanner/InstanceTest.php +++ b/tests/snippets/Spanner/InstanceTest.php @@ -25,7 +25,6 @@ use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; -use Google\Cloud\Spanner\Session\SessionPoolInterface; use Prophecy\Argument; /** @@ -46,7 +45,6 @@ public function setUp() $this->connection = $this->prophesize(ConnectionInterface::class); $this->instance = \Google\Cloud\Dev\stub(Instance::class, [ $this->connection->reveal(), - $this->prophesize(SessionPoolInterface::class)->reveal(), $this->prophesize(LongRunningConnectionInterface::class)->reveal(), [], self::PROJECT, diff --git a/tests/snippets/Spanner/ResultTest.php b/tests/snippets/Spanner/ResultTest.php new file mode 100644 index 000000000000..9315b1e56a25 --- /dev/null +++ b/tests/snippets/Spanner/ResultTest.php @@ -0,0 +1,110 @@ +prophesize(Result::class); + $database = $this->prophesize(Database::class); + $result->rows() + ->willReturn($this->resultGenerator()); + $result->metadata() + ->willReturn([]); + $result->snapshot() + ->willReturn($this->prophesize(Snapshot::class)->reveal()); + $result->transaction() + ->willReturn($this->prophesize(Transaction::class)->reveal()); + $this->result = $result->reveal(); + $database->execute(Argument::any()) + ->willReturn($this->result); + $this->database = $database->reveal(); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Result::class); + $snippet->replace('$database =', '//$database ='); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke('result'); + $this->assertInstanceOf(Result::class, $res->returnVal()); + } + + public function testRows() + { + $snippet = $this->snippetFromMethod(Result::class, 'rows'); + $snippet->addLocal('result', $this->result); + $res = $snippet->invoke('rows'); + $this->assertInstanceOf(\Generator::class, $res->returnVal()); + } + + public function testMetadata() + { + $snippet = $this->snippetFromMethod(Result::class, 'metadata'); + $snippet->addLocal('result', $this->result); + $res = $snippet->invoke('metadata'); + $this->assertInternalType('array', $res->returnVal()); + } + + public function testStats() + { + $snippet = $this->snippetFromMethod(Result::class, 'metadata'); + $snippet->addLocal('result', $this->result); + $res = $snippet->invoke('metadata'); + $this->assertInternalType('array', $res->returnVal()); + } + + public function testSnapshot() + { + $snippet = $this->snippetFromMethod(Result::class, 'snapshot'); + $snippet->addLocal('result', $this->result); + $res = $snippet->invoke('snapshot'); + $this->assertInstanceOf(Snapshot::class, $res->returnVal()); + } + + public function testTransaction() + { + $snippet = $this->snippetFromMethod(Result::class, 'transaction'); + $snippet->addLocal('result', $this->result); + $res = $snippet->invoke('transaction'); + $this->assertInstanceOf(Transaction::class, $res->returnVal()); + } + + private function resultGenerator() + { + yield []; + } +} diff --git a/tests/snippets/Spanner/SnapshotTest.php b/tests/snippets/Spanner/SnapshotTest.php index 86d7ed889725..11b24f6e9cbd 100644 --- a/tests/snippets/Spanner/SnapshotTest.php +++ b/tests/snippets/Spanner/SnapshotTest.php @@ -82,9 +82,9 @@ public function testClass() public function testExecute() { - $this->connection->executeSql(Argument::any()) + $this->connection->executeStreamingSql(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -97,12 +97,8 @@ public function testExecute() ] ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'values' => [0] + ])); $this->stubOperation(); @@ -115,9 +111,9 @@ public function testExecute() public function testRead() { - $this->connection->read(Argument::any()) + $this->connection->streamingRead(Argument::any()) ->shouldBeCalled() - ->willReturn([ + ->willReturn($this->resultGenerator([ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -130,12 +126,8 @@ public function testRead() ] ] ], - 'rows' => [ - [ - 0 - ] - ] - ]); + 'values' => [0] + ])); $this->stubOperation(); @@ -164,4 +156,9 @@ public function testReadTimestamp() $res = $snippet->invoke('timestamp'); $this->assertInstanceOf(Timestamp::class, $res->returnVal()); } + + private function resultGenerator(array $data) + { + yield $data; + } } diff --git a/tests/snippets/Spanner/SpannerClientTest.php b/tests/snippets/Spanner/SpannerClientTest.php index b715cf0f262f..125a6c9f57af 100644 --- a/tests/snippets/Spanner/SpannerClientTest.php +++ b/tests/snippets/Spanner/SpannerClientTest.php @@ -30,7 +30,6 @@ use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Spanner\Session\SessionClient; use Google\Cloud\Spanner\SpannerClient; use Google\Cloud\Spanner\Timestamp; use Prophecy\Argument; @@ -252,15 +251,6 @@ public function testDuration() $this->assertInstanceOf(Duration::class, $res->returnVal()); } - public function testSessionClient() - { - $snippet = $this->snippetFromMethod(SpannerClient::class, 'sessionClient'); - $snippet->addLocal('spanner', $this->client); - - $res = $snippet->invoke('sessionClient'); - $this->assertInstanceOf(SessionClient::class, $res->returnVal()); - } - public function testResumeOperation() { $snippet = $this->snippetFromMethod(SpannerClient::class, 'resumeOperation'); diff --git a/tests/snippets/Spanner/TransactionTest.php b/tests/snippets/Spanner/TransactionTest.php index f0733e22545d..881adc9f9ca1 100644 --- a/tests/snippets/Spanner/TransactionTest.php +++ b/tests/snippets/Spanner/TransactionTest.php @@ -92,27 +92,9 @@ public function testClassReturnTransaction() public function testExecute() { - $this->connection->executeSql(Argument::any()) + $this->connection->executeStreamingSql(Argument::any()) ->shouldBeCalled() - ->willReturn([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'loginCount', - 'type' => [ - 'code' => ValueMapper::TYPE_INT64 - ] - ] - ] - ] - ], - 'rows' => [ - [ - 0 - ] - ] - ]); + ->willReturn($this->resultGenerator()); $this->stubOperation(); @@ -125,27 +107,9 @@ public function testExecute() public function testRead() { - $this->connection->read(Argument::any()) + $this->connection->streamingRead(Argument::any()) ->shouldBeCalled() - ->willReturn([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'loginCount', - 'type' => [ - 'code' => ValueMapper::TYPE_INT64 - ] - ] - ] - ] - ], - 'rows' => [ - [ - 0 - ] - ] - ]); + ->willReturn($this->resultGenerator()); $this->stubOperation(); @@ -327,4 +291,23 @@ public function testState() $res = $snippet->invoke('state'); $this->assertEquals(Transaction::STATE_ACTIVE, $res->returnVal()); } + + private function resultGenerator() + { + yield [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'values' => [0] + ]; + } } diff --git a/tests/snippets/Speech/SpeechClientTest.php b/tests/snippets/Speech/SpeechClientTest.php index 03c6fd70c636..5621d9527f99 100644 --- a/tests/snippets/Speech/SpeechClientTest.php +++ b/tests/snippets/Speech/SpeechClientTest.php @@ -36,7 +36,9 @@ public function setUp() { $this->testFile = "'" . __DIR__ . '/../fixtures/Speech/demo.flac' . "'"; $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = \Google\Cloud\Dev\stub(SpeechClient::class, ['languageCode' => 'en-US']); + $this->client = \Google\Cloud\Dev\stub(SpeechClient::class, [ + ['languageCode' => 'en-US'] + ]); $this->client->___setProperty('connection', $this->connection->reveal()); } diff --git a/tests/system/Spanner/OperationsTest.php b/tests/system/Spanner/OperationsTest.php index fcfb952858f2..f5113845eec5 100644 --- a/tests/system/Spanner/OperationsTest.php +++ b/tests/system/Spanner/OperationsTest.php @@ -63,7 +63,7 @@ public function testRead() $columns = ['id', 'name']; $res = $db->read(self::TEST_TABLE_NAME, $keySet, $columns); - $row = $res->firstRow(); + $row = $res->rows()->current(); $this->assertEquals($this->id, $row['id']); } @@ -137,6 +137,6 @@ private function getRow() ] ]); - return $res->firstRow(); + return $res->rows()->current(); } } diff --git a/tests/system/Spanner/SnapshotTest.php b/tests/system/Spanner/SnapshotTest.php index d6f6e78cf784..06becac07d3b 100644 --- a/tests/system/Spanner/SnapshotTest.php +++ b/tests/system/Spanner/SnapshotTest.php @@ -48,10 +48,12 @@ public function testSnapshot() private function getRow($client) { - return $client->execute('SELECT * FROM Users WHERE id=@id', [ + $result = $client->execute('SELECT * FROM Users WHERE id=@id', [ 'parameters' => [ 'id' => $this->id ] - ])->firstRow(); + ]); + + return $result->rows()->current(); } } diff --git a/tests/unit/Core/PhpArrayTest.php b/tests/unit/Core/PhpArrayTest.php index 1bb285b5190e..a7c5de6e2edf 100644 --- a/tests/unit/Core/PhpArrayTest.php +++ b/tests/unit/Core/PhpArrayTest.php @@ -98,7 +98,7 @@ class TestMessage extends Message { public $test_struct = null; public $test_labels = []; - public $test_stings = []; + public $test_strings = []; protected static $__extensions = array(); diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php index 5216a69fc6f0..0dffb84bc936 100644 --- a/tests/unit/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/DatabaseTest.php @@ -64,7 +64,7 @@ public function setUp() $this->lro = $this->prophesize(LongRunningConnectionInterface::class); $this->lroCallables = []; - $this->sessionPool->session(self::INSTANCE, self::DATABASE, Argument::any()) + $this->sessionPool->acquire(Argument::type('string')) ->willReturn(new Session( $this->connection->reveal(), self::PROJECT, @@ -72,17 +72,21 @@ public function setUp() self::DATABASE, self::SESSION )); + $this->sessionPool->setDatabase(Argument::type(Database::class)) + ->willReturn(null); + $this->sessionPool->release(Argument::type(Session::class)) + ->willReturn(null); $this->instance->name()->willReturn(self::INSTANCE); $args = [ $this->connection->reveal(), $this->instance->reveal(), - $this->sessionPool->reveal(), $this->lro->reveal(), $this->lroCallables, self::PROJECT, self::DATABASE, + $this->sessionPool->reveal() ]; $props = [ @@ -528,35 +532,18 @@ public function testExecute() { $sql = 'SELECT * FROM Table'; - $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { + $this->connection->executeStreamingSql(Argument::that(function ($arg) use ($sql) { if ($arg['sql'] !== $sql) return false; return true; - }))->shouldBeCalled()->willReturn([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'ID', - 'type' => [ - 'code' => ValueMapper::TYPE_INT64 - ] - ] - ] - ] - ], - 'rows' => [ - [ - '10' - ] - ] - ]); + }))->shouldBeCalled()->willReturn($this->resultGenerator()); $this->refreshOperation(); $res = $this->database->execute($sql); $this->assertInstanceOf(Result::class, $res); - $this->assertEquals(10, $res->rows()[0]['ID']); + $rows = iterator_to_array($res->rows()); + $this->assertEquals(10, $rows[0]['ID']); } public function testRead() @@ -564,13 +551,28 @@ public function testRead() $table = 'Table'; $opts = ['foo' => 'bar']; - $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { + $this->connection->streamingRead(Argument::that(function ($arg) use ($table, $opts) { if ($arg['table'] !== $table) return false; if ($arg['keySet']['all'] !== true) return false; if ($arg['columns'] !== ['ID']) return false; return true; - }))->shouldBeCalled()->willReturn([ + }))->shouldBeCalled()->willReturn($this->resultGenerator()); + + $this->refreshOperation(); + + $res = $this->database->read($table, new KeySet(['all' => true]), ['ID']); + $this->assertInstanceOf(Result::class, $res); + $rows = iterator_to_array($res->rows()); + $this->assertEquals(10, $rows[0]['ID']); + } + + // ******* + // Helpers + + private function resultGenerator() + { + yield [ 'metadata' => [ 'rowType' => [ 'fields' => [ @@ -583,23 +585,12 @@ public function testRead() ] ] ], - 'rows' => [ - [ - '10' - ] + 'values' => [ + '10' ] - ]); - - $this->refreshOperation(); - - $res = $this->database->read($table, new KeySet(['all' => true]), ['ID']); - $this->assertInstanceOf(Result::class, $res); - $this->assertEquals(10, $res->rows()[0]['ID']); + ]; } - // ******* - // Helpers - private function refreshOperation() { $operation = new Operation($this->connection->reveal(), false); diff --git a/tests/unit/Spanner/InstanceTest.php b/tests/unit/Spanner/InstanceTest.php index 5612ce712733..90533742c67d 100644 --- a/tests/unit/Spanner/InstanceTest.php +++ b/tests/unit/Spanner/InstanceTest.php @@ -48,7 +48,6 @@ public function setUp() $this->connection = $this->prophesize(ConnectionInterface::class); $this->instance = \Google\Cloud\Dev\stub(Instance::class, [ $this->connection->reveal(), - $this->prophesize(SessionPoolInterface::class)->reveal(), $this->prophesize(LongRunningConnectionInterface::class)->reveal(), [], self::PROJECT_ID, diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php index 298e8531632a..e111d718959f 100644 --- a/tests/unit/Spanner/OperationTest.php +++ b/tests/unit/Spanner/OperationTest.php @@ -159,7 +159,7 @@ public function testExecute() $sql = 'SELECT * FROM Posts WHERE ID = @id'; $params = ['id' => 10]; - $this->connection->executeSql(Argument::that(function ($arg) use ($sql, $params) { + $this->connection->executeStreamingSql(Argument::that(function ($arg) use ($sql, $params) { if ($arg['sql'] !== $sql) return false; if ($arg['session'] !== self::SESSION) return false; if ($arg['params'] !== ['id' => '10']) return false; @@ -175,12 +175,13 @@ public function testExecute() ]); $this->assertInstanceOf(Result::class, $res); - $this->assertEquals(10, $res->rows()[0]['ID']); + $rows = iterator_to_array($res->rows()); + $this->assertEquals(10, $rows[0]['ID']); } public function testRead() { - $this->connection->read(Argument::that(function ($arg) { + $this->connection->streamingRead(Argument::that(function ($arg) { if ($arg['table'] !== 'Posts') return false; if ($arg['session'] !== self::SESSION) return false; if ($arg['keySet']['all'] !== true) return false; @@ -193,12 +194,13 @@ public function testRead() $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo']); $this->assertInstanceOf(Result::class, $res); - $this->assertEquals(10, $res->rows()[0]['ID']); + $rows = iterator_to_array($res->rows()); + $this->assertEquals(10, $rows[0]['ID']); } public function testReadWithTransaction() { - $this->connection->read(Argument::that(function ($arg) { + $this->connection->streamingRead(Argument::that(function ($arg) { if ($arg['table'] !== 'Posts') return false; if ($arg['session'] !== self::SESSION) return false; if ($arg['keySet']['all'] !== true) return false; @@ -214,13 +216,15 @@ public function testReadWithTransaction() $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ 'transactionContext' => SessionPoolInterface::CONTEXT_READWRITE ]); + $res->rows()->next(); + $this->assertInstanceOf(Transaction::class, $res->transaction()); $this->assertEquals(self::TRANSACTION, $res->transaction()->id()); } public function testReadWithSnapshot() { - $this->connection->read(Argument::that(function ($arg) { + $this->connection->streamingRead(Argument::that(function ($arg) { if ($arg['table'] !== 'Posts') return false; if ($arg['session'] !== self::SESSION) return false; if ($arg['keySet']['all'] !== true) return false; @@ -236,6 +240,8 @@ public function testReadWithSnapshot() $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ 'transactionContext' => SessionPoolInterface::CONTEXT_READ ]); + $res->rows()->next(); + $this->assertInstanceOf(Snapshot::class, $res->snapshot()); $this->assertEquals(self::TRANSACTION, $res->snapshot()->id()); } @@ -282,7 +288,7 @@ public function testSnapshotWithTimestamp() private function executeAndReadResponse(array $additionalMetadata = []) { - return [ + yield [ 'metadata' => array_merge([ 'rowType' => [ 'fields' => [ @@ -295,8 +301,8 @@ private function executeAndReadResponse(array $additionalMetadata = []) ] ] ], $additionalMetadata), - 'rows' => [ - ['10'] + 'values' => [ + '10' ] ]; } diff --git a/tests/unit/Spanner/ResultTest.php b/tests/unit/Spanner/ResultTest.php index 98fa6d8ef6b5..f61fe1d08cc0 100644 --- a/tests/unit/Spanner/ResultTest.php +++ b/tests/unit/Spanner/ResultTest.php @@ -17,90 +17,135 @@ namespace Google\Cloud\Tests\Spanner; +use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Session\Session; +use Google\Cloud\Spanner\Snapshot; +use Google\Cloud\Spanner\Transaction; +use Google\Cloud\Spanner\ValueMapper; +use Prophecy\Argument; /** * @group spanner */ class ResultTest extends \PHPUnit_Framework_TestCase { - public function testIterator() + /** + * @dataProvider streamingDataProvider + */ + public function testRows($chunks, $expectedValues) { - $result = new Result([], [ - ['name' => 'John'] - ]); + $result = iterator_to_array($this->getResultClass($chunks)); - $res = iterator_to_array($result); - $this->assertEquals(1, count($res)); - $this->assertEquals('John', $res[0]['name']); + $this->assertEquals($expectedValues, $result); } - public function testMetadata() + public function streamingDataProvider() { - $result = new Result(['metadata' => 'foo'], []); - $this->assertEquals('foo', $result->metadata()); + foreach ($this->getStreamingDataFixture()['tests'] as $test) { + yield [$test['chunks'], $test['result']['value']]; + } } - public function testRows() + public function testIterator() { - $rows = [ - ['name' => 'John'] - ]; + $fixture = $this->getStreamingDataFixture()['tests'][0]; + $result = iterator_to_array($this->getResultClass($fixture['chunks'])); - $result = new Result([], $rows); - - $this->assertEquals($rows, $result->rows()); + $this->assertEquals($fixture['result']['value'], $result); } - public function testFirstRow() + public function testMetadata() { - $rows = [ - ['name' => 'John'], - ['name' => 'Dave'] - ]; + $fixture = $this->getStreamingDataFixture()['tests'][0]; + $result = $this->getResultClass($fixture['chunks']); + $expectedMetadata = json_decode($fixture['chunks'][0], true)['metadata']; - $result = new Result([], $rows); - - $this->assertEquals($rows[0], $result->firstRow()); + $this->assertNull($result->stats()); + $result->rows()->next(); + $this->assertEquals($expectedMetadata, $result->metadata()); } public function testStats() { - $result = new Result(['stats' => 'foo'], []); - $this->assertEquals('foo', $result->stats()); - } + $fixture = $this->getStreamingDataFixture()['tests'][1]; + $result = $this->getResultClass($fixture['chunks']); + $expectedStats = json_decode($fixture['chunks'][0], true)['stats']; - public function testInfo() - { - $info = ['foo' => 'bar']; - $result = new Result($info, []); - - $this->assertEquals($info, $result->info()); + $this->assertNull($result->stats()); + $result->rows()->next(); + $this->assertEquals($expectedStats, $result->stats()); } public function testTransaction() { - $result = new Result([], [], [ - 'transaction' => 'foo' - ]); - - $this->assertEquals('foo', $result->transaction()); - - $result = new Result([], []); + $fixture = $this->getStreamingDataFixture()['tests'][1]; + $result = $this->getResultClass($fixture['chunks'], 'rw'); $this->assertNull($result->transaction()); + $result->rows()->next(); + $this->assertInstanceOf(Transaction::class, $result->transaction()); } public function testSnapshot() { - $result = new Result([], [], [ - 'snapshot' => 'foo' - ]); + $fixture = $this->getStreamingDataFixture()['tests'][1]; + $result = $this->getResultClass($fixture['chunks']); - $this->assertEquals('foo', $result->snapshot()); + $this->assertNull($result->snapshot()); + $result->rows()->next(); + $this->assertInstanceOf(Snapshot::class, $result->snapshot()); + } + + private function getResultClass($chunks, $context = 'r') + { + $operation = $this->prophesize(Operation::class); + $session = $this->prophesize(Session::class)->reveal(); + $mapper = $this->prophesize(ValueMapper::class); + $transaction = $this->prophesize(Transaction::class); + $snapshot = $this->prophesize(Snapshot::class); + $mapper->decodeValues( + Argument::any(), + Argument::any() + )->will(function ($args) { + return $args[1]; + }); + + if ($context === 'r') { + $operation->createSnapshot( + $session, + Argument::type('array') + )->willReturn($snapshot->reveal()); + } else { + $operation->createTransaction( + $session, + Argument::type('array') + )->willReturn($transaction->reveal()); + } + + return new Result( + $operation->reveal(), + $session, + function () use ($chunks) { + return $this->resultGenerator($chunks); + }, + $context, + $mapper->reveal() + ); + } - $result = new Result([], []); + private function resultGenerator($chunks) + { + foreach ($chunks as $chunk) { + yield json_decode($chunk, true); + } + } - $this->assertNull($result->snapshot()); + private function getStreamingDataFixture() + { + return json_decode( + file_get_contents(__DIR__ .'/../fixtures/spanner/streaming-read-acceptance-test.json'), + true + ); } } diff --git a/tests/unit/Spanner/Session/CacheSessionPoolTest.php b/tests/unit/Spanner/Session/CacheSessionPoolTest.php new file mode 100644 index 000000000000..47a494536233 --- /dev/null +++ b/tests/unit/Spanner/Session/CacheSessionPoolTest.php @@ -0,0 +1,471 @@ +time = time(); + } + + /** + * @dataProvider badConfigDataProvider + */ + public function testThrowsExceptionWithInvalidConfig($config) + { + $exceptionThrown = false; + + try { + new CacheSessionPool($this->getCacheItemPool(), $config); + } catch (\InvalidArgumentException $ex) { + $exceptionThrown = true; + } + + $this->assertTrue($exceptionThrown); + } + + public function badConfigDataProvider() + { + return [ + [['maxSessions' => -1]], + [['minSessions' => -1]], + [['maxCyclesToWaitForSession' => -1]], + [['sleepIntervalSeconds' => -1]], + [['minSessions' => 5, 'maxSessions' => 1]], + [['lock' => new \stdClass]] + ]; + } + + /** + * @expectedException \RuntimeException + */ + public function testAcquireThrowsExceptionWhenMaxCyclesMet() + { + $config = [ + 'maxSessions' => 1, + 'maxCyclesToWaitForSession' => 1 + ]; + $cacheData = [ + 'queue' => [], + 'inUse' => [ + 'alreadyCheckedOut' => [ + 'name' => 'alreadyCheckedOut', + 'expiration' => $this->time + 3600, + 'lastActive' => $this->time + ] + ], + 'toCreate' => [] + ]; + $pool = new CacheSessionPoolStub($this->getCacheItemPool($cacheData), $config, $this->time); + $pool->setDatabase($this->getDatabase()); + $pool->acquire(); + } + + /** + * @expectedException \RuntimeException + */ + public function testAcquireThrowsExceptionWithNoAvailableSessions() + { + $config = [ + 'maxSessions' => 1, + 'shouldWaitForSession' => false + ]; + $cacheData = [ + 'queue' => [], + 'inUse' => [ + 'alreadyCheckedOut' => [ + 'name' => 'alreadyCheckedOut', + 'expiration' => $this->time + 3600, + 'lastActive' => $this->time + ] + ], + 'toCreate' => [] + ]; + $pool = new CacheSessionPoolStub($this->getCacheItemPool($cacheData), $config, $this->time); + $pool->setDatabase($this->getDatabase()); + $pool->acquire(); + } + + public function testAcquireRemovesToCreateItemsIfCreateCallFails() + { + $exceptionThrown = false; + $config = ['maxSessions' => 1]; + $pool = new CacheSessionPoolStub($this->getCacheItemPool(), $config, $this->time); + $pool->setDatabase($this->getDatabase(true)); + + try { + $actualSession = $pool->acquire(); + } catch (\Exception $ex) { + $exceptionThrown = true; + } + + $actualItemPool = $pool->cacheItemPool(); + $actualCacheData = $actualItemPool->getItem( + sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + )->get(); + + $this->assertEmpty($actualCacheData['toCreate']); + $this->assertTrue($exceptionThrown); + } + + public function testRelease() + { + $cacheData = [ + 'queue' => [], + 'inUse' => [ + 'session' => [ + 'name' => 'session', + 'expiration' => $this->time + 3600, + 'lastActive' => $this->time + ] + ], + 'toCreate' => [] + ]; + $expectedCacheData = [ + 'queue' => [ + [ + 'name' => 'session', + 'expiration' => $this->time + 3600 + ] + ], + 'inUse' => [], + 'toCreate' => [] + ]; + $session = $this->prophesize(Session::class); + $session->name() + ->willReturn('session'); + $session->expiration() + ->willReturn($this->time + 3600); + $pool = new CacheSessionPoolStub($this->getCacheItemPool($cacheData), [], $this->time); + $pool->setDatabase($this->getDatabase()); + $pool->release($session->reveal()); + $actualItemPool = $pool->cacheItemPool(); + $actualCacheData = $actualItemPool->getItem( + sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + )->get(); + + $this->assertEquals($expectedCacheData, $actualCacheData); + } + + public function testClearPool() + { + $pool = new CacheSessionPoolStub($this->getCacheItemPool()); + $pool->clear(); + $actualItemPool = $pool->cacheItemPool(); + $actualCacheData = $actualItemPool->getItem( + sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + )->get(); + + $this->assertNull($actualCacheData); + } + + /** + * @dataProvider acquireDataProvider + */ + public function testAcquire($config, $cacheData, $expectedCacheData, $time) + { + $pool = new CacheSessionPoolStub($this->getCacheItemPool($cacheData), $config, $time); + $pool->setDatabase($this->getDatabase()); + $actualSession = $pool->acquire(); + $actualItemPool = $pool->cacheItemPool(); + $actualCacheData = $actualItemPool->getItem( + sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + )->get(); + + $this->assertInstanceOf(Session::class, $actualSession); + $this->assertEquals($expectedCacheData, $actualCacheData); + } + + public function acquireDataProvider() + { + $time = time(); + + return [ + // Set #0: Initialize data using default config + [ + [], + null, + [ + 'queue' => [], + 'inUse' => [ + 'session0' => [ + 'name' => 'session0', + 'expiration' => $time + 3600, + 'lastActive' => $time + ] + ], + 'toCreate' => [] + ], + $time + ], + // Set #1: Purge expired session from queue and create sessions up to min + [ + ['minSessions' => 3], + [ + 'queue' => [ + [ + 'name' => 'expired', + 'expiration' => $time - 3000 + ], + [ + 'name' => 'stillValid', + 'expiration' => $time + 3600 + ] + ], + 'inUse' => [], + 'toCreate' => [] + ], + [ + 'queue' => [ + [ + 'name' => 'session0', + 'expiration' => $time + 3600 + ], + [ + 'name' => 'session1', + 'expiration' => $time + 3600 + ] + ], + 'inUse' => [ + 'stillValid' => [ + 'name' => 'stillValid', + 'expiration' => $time + 3600, + 'lastActive' => $time + ] + ], + 'toCreate' => [] + ], + $time + ], + // // Set #2: Create a new session when all available are checked out + // // and we have not reached the max limit + [ + [], + [ + 'queue' => [], + 'inUse' => [ + 'alreadyCheckedOut' => [ + 'name' => 'alreadyCheckedOut', + 'expiration' => $time + 3600, + 'lastActive' => $time + ] + ], + 'toCreate' => [] + ], + [ + 'queue' => [], + 'inUse' => [ + 'session0' => [ + 'name' => 'session0', + 'expiration' => $time + 3600, + 'lastActive' => $time + ], + 'alreadyCheckedOut' => [ + 'name' => 'alreadyCheckedOut', + 'expiration' => $time + 3600, + 'lastActive' => $time + ] + ], + 'toCreate' => [] + ], + $time + ], + // Set #3: Run clean up on abandoned items and create new + [ + ['maxSessions' => 3], + [ + 'queue' => [], + 'inUse' => [ + 'expiredInUse1' => [ + 'name' => 'expiredInUse1', + 'expiration' => $time - 5000, + 'lastActive' => $time - 1201 + ], + 'expiredInUse2' => [ + 'name' => 'expiredInUse2', + 'expiration' => $time - 5000, + 'lastActive' => $time - 1201 + ] + ], + 'toCreate' => [ + 'oldguy' => $time - 1201 + ] + ], + [ + 'queue' => [], + 'inUse' => [ + 'session0' => [ + 'name' => 'session0', + 'expiration' => $time + 3600, + 'lastActive' => $time + ] + ], + 'toCreate' => [] + ], + $time + ], + // Set #4: Basic test, check out session from queue + [ + [], + [ + 'queue' => [ + [ + 'name' => 'session', + 'expiration' => $time + 3600 + ] + ], + 'inUse' => [], + 'toCreate' => [] + ], + [ + 'queue' => [], + 'inUse' => [ + 'session' => [ + 'name' => 'session', + 'expiration' => $time + 3600, + 'lastActive' => $time + ] + ], + 'toCreate' => [] + ], + $time + ], + // Set #5: Session expires in a half hour, check validity against API + [ + [], + [ + 'queue' => [ + [ + 'name' => 'expiresSoon', + 'expiration' => $time + 1500 + ], + [ + 'name' => 'session', + 'expiration' => $time + 3600 + ] + ], + 'inUse' => [], + 'toCreate' => [] + ], + [ + 'queue' => [], + 'inUse' => [ + 'session' => [ + 'name' => 'session', + 'expiration' => $time + 3600, + 'lastActive' => $time + ] + ], + 'toCreate' => [] + ], + $time + ] + ]; + } + + private function getDatabase($shouldCreateThrowException = false) + { + $database = $this->prophesize(Database::class); + $createdSession = $this->prophesize(Session::class); + $session = $this->prophesize(Session::class); + $createdSession->expiration() + ->willReturn($this->time + 3600); + $session->expiration() + ->willReturn($this->time + 3600); + $session->exists() + ->willReturn(false); + $database->session(Argument::any()) + ->will(function ($args) use ($session) { + $session->name() + ->willReturn($args[0]); + + return $session->reveal(); + }); + $database->identity() + ->willReturn([ + 'database' => self::DATABASE_NAME, + 'instance' => self::INSTANCE_NAME + ]); + + if ($shouldCreateThrowException) { + $database->createSession() + ->willThrow(new \Exception()); + } else { + $database->createSession() + ->will(function ($args, $mock, $method) use ($createdSession) { + $methodCalls = $mock->findProphecyMethodCalls( + $method->getMethodName(), + new ArgumentsWildcard($args) + ); + + $createdSession->name() + ->willReturn('session' . count($methodCalls)); + + return $createdSession; + }); + } + + return $database->reveal(); + } + + private function getCacheItemPool(array $cacheData = null) + { + $cacheItemPool = new MemoryCacheItemPool(); + $cacheItem = $cacheItemPool->getItem( + sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + ); + $cacheItemPool->save($cacheItem->set($cacheData)); + + return $cacheItemPool; + } +} + +class CacheSessionPoolStub extends CacheSessionPool +{ + private $time; + + public function __construct(CacheItemPoolInterface $cacheItemPool, array $config = [], $time = null) + { + $this->time = $time; + parent::__construct($cacheItemPool, $config); + } + + protected function time() + { + return $this->time ?: parent::$this->time(); + } +} diff --git a/tests/unit/Spanner/SpannerClientTest.php b/tests/unit/Spanner/SpannerClientTest.php index b58b8ff37b90..e7006cbe2a68 100644 --- a/tests/unit/Spanner/SpannerClientTest.php +++ b/tests/unit/Spanner/SpannerClientTest.php @@ -29,7 +29,6 @@ use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Spanner\Session\SessionClient; use Google\Cloud\Spanner\SpannerClient; use Google\Cloud\Spanner\Timestamp; use Prophecy\Argument; @@ -279,10 +278,4 @@ public function testDuration() $d = $this->client->duration(10, 1); $this->assertInstanceOf(Duration::class, $d); } - - public function testSessionClient() - { - $sc = $this->client->sessionClient(); - $this->assertInstanceOf(SessionClient::class, $sc); - } } diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php index 867ae378dbb2..db5e71d9ad0d 100644 --- a/tests/unit/Spanner/TransactionTest.php +++ b/tests/unit/Spanner/TransactionTest.php @@ -175,36 +175,19 @@ public function testExecute() { $sql = 'SELECT * FROM Table'; - $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { + $this->connection->executeStreamingSql(Argument::that(function ($arg) use ($sql) { if ($arg['transaction']['id'] !== self::TRANSACTION) return false; if ($arg['sql'] !== $sql) return false; return true; - }))->shouldBeCalled()->willReturn([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'ID', - 'type' => [ - 'code' => ValueMapper::TYPE_INT64 - ] - ] - ] - ] - ], - 'rows' => [ - [ - '10' - ] - ] - ]); + }))->shouldBeCalled()->willReturn($this->resultGenerator()); $this->refreshOperation(); $res = $this->transaction->execute($sql); $this->assertInstanceOf(Result::class, $res); - $this->assertEquals(10, $res->rows()[0]['ID']); + $rows = iterator_to_array($res->rows()); + $this->assertEquals(10, $rows[0]['ID']); } public function testRead() @@ -212,38 +195,22 @@ public function testRead() $table = 'Table'; $opts = ['foo' => 'bar']; - $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { + $this->connection->streamingRead(Argument::that(function ($arg) use ($table, $opts) { if ($arg['transaction']['id'] !== self::TRANSACTION) return false; if ($arg['table'] !== $table) return false; if ($arg['keySet']['all'] !== true) return false; if ($arg['columns'] !== ['ID']) return false; return true; - }))->shouldBeCalled()->willReturn([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'ID', - 'type' => [ - 'code' => ValueMapper::TYPE_INT64 - ] - ] - ] - ] - ], - 'rows' => [ - [ - '10' - ] - ] - ]); + }))->shouldBeCalled()->willReturn($this->resultGenerator()); $this->refreshOperation(); $res = $this->transaction->read($table, new KeySet(['all' => true]), ['ID']); + $this->assertInstanceOf(Result::class, $res); - $this->assertEquals(10, $res->rows()[0]['ID']); + $rows = iterator_to_array($res->rows()); + $this->assertEquals(10, $rows[0]['ID']); } public function testCommit() @@ -304,6 +271,27 @@ public function testState() // ******* // Helpers + private function resultGenerator() + { + yield [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'values' => [ + '10' + ] + ]; + } + private function refreshOperation() { $operation = new Operation($this->connection->reveal(), false); diff --git a/tests/unit/fixtures/spanner/streaming-read-acceptance-test.json b/tests/unit/fixtures/spanner/streaming-read-acceptance-test.json new file mode 100644 index 000000000000..718e782c9839 --- /dev/null +++ b/tests/unit/fixtures/spanner/streaming-read-acceptance-test.json @@ -0,0 +1,371 @@ +{ + "tests": [ + { + "result": { + "value": [ + [ + true, + "abc", + "100", + 1.1, + "YWJj", + [ + "abc", + "def", + null, + "ghi" + ], + [ + [ + "abc" + ], + [ + "def" + ], + [ + "ghi" + ] + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 1\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": 6\n }\n }, {\n \"name\": \"f3\",\n \"type\": {\n \"code\": 2\n }\n }, {\n \"name\": \"f4\",\n \"type\": {\n \"code\": 3\n }\n }, {\n \"name\": \"f5\",\n \"type\": {\n \"code\": 7\n }\n }, {\n \"name\": \"f6\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 6\n }\n }\n }, {\n \"name\": \"f7\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 9,\n \"structType\": {\n \"fields\": [{\n \"name\": \"f71\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [true, \"abc\", \"100\", 1.1, \"YWJj\", [\"abc\", \"def\", null, \"ghi\"], [[\"abc\"], [\"def\"], [\"ghi\"]]]\n}" + ], + "name": "Basic Test" + }, + { + "result": { + "value": [ + [ + true, + "abc", + "100", + 1.1, + "YWJj", + [ + "abc", + "def", + null, + "ghi" + ], + [ + [ + "abc" + ], + [ + "def" + ], + [ + "ghi" + ] + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"transaction\": {\n \"id\": 1},\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 1\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": 6\n }\n }, {\n \"name\": \"f3\",\n \"type\": {\n \"code\": 2\n }\n }, {\n \"name\": \"f4\",\n \"type\": {\n \"code\": 3\n }\n }, {\n \"name\": \"f5\",\n \"type\": {\n \"code\": 7\n }\n }, {\n \"name\": \"f6\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 6\n }\n }\n }, {\n \"name\": \"f7\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 9,\n \"structType\": {\n \"fields\": [{\n \"name\": \"f71\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"stats\": true,\n \"values\": [true, \"abc\", \"100\", 1.1, \"YWJj\", [\"abc\", \"def\", null, \"ghi\"], [[\"abc\"], [\"def\"], [\"ghi\"]]]\n}" + ], + "name": "Basic Test w/ stats and transaction id" + }, + { + "result": { + "value": [ + [ + "abcdefghi" + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n },\n \"values\": [\"abc\"],\n \"chunkedValue\": true\n}", + "{\n \"values\": [\"def\"],\n \"chunkedValue\": true\n}", + "{\n \"values\": [\"ghi\"]\n}" + ], + "name": "String Chunking Test" + }, + { + "result": { + "value": [ + [ + [ + "abc", + "def", + "ghi", + "jkl" + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 6\n }\n }\n }]\n }\n },\n \"values\": [[\"abc\", \"d\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"ef\", \"gh\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"i\", \"jkl\"]]\n}" + ], + "name": "String Array Chunking Test" + }, + { + "result": { + "value": [ + [ + [ + "abc", + "def", + null, + "ghi", + null, + "jkl" + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 6\n }\n }\n }]\n }\n },\n \"values\": [[\"abc\", \"def\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[null, \"ghi\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[null, \"jkl\"]]\n}" + ], + "name": "String Array Chunking Test With Nulls" + }, + { + "result": { + "value": [ + [ + [ + "abc", + "def", + "ghi", + "jkl" + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 6\n }\n }\n }]\n }\n },\n \"values\": [[\"abc\", \"def\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"\", \"ghi\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"\", \"jkl\"]]\n}" + ], + "name": "String Array Chunking Test With Empty Strings" + }, + { + "result": { + "value": [ + [ + [ + "abcdefghi" + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 6\n }\n }\n }]\n }\n },\n \"values\": [[\"abc\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"def\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"ghi\"]]\n}" + ], + "name": "String Array Chunking Test With One Large String" + }, + { + "result": { + "value": [ + [ + [ + "1", + "23", + "4", + null, + 5 + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 2\n }\n }\n }]\n }\n },\n \"values\": [[\"1\", \"2\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"3\", \"4\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"\", null, \"5\"]]\n}" + ], + "name": "INT64 Array Chunking Test" + }, + { + "result": { + "value": [ + [ + [ + 1, + 2, + "Infinity", + "-Infinity", + "NaN", + null, + 3 + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 3\n }\n }\n }]\n }\n },\n \"values\": [[1.0, 2.0]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"Infinity\", \"-Infinity\", \"NaN\"]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[\"\", null, 3.0]]\n}" + ], + "name": "FLOAT64 Array Chunking Test" + }, + { + "result": { + "value": [ + [ + [ + [ + "abc", + "defghi" + ], + [ + "123", + "456" + ] + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 9,\n \"structType\": {\n \"fields\": [{\n \"name\": \"f11\",\n \"type\": {\n \"code\": 6\n }\n }, {\n \"name\": \"f12\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [[[\"abc\", \"def\"]]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[[\"ghi\"], [\"123\", \"456\"]]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[[\"\"]]]\n}" + ], + "name": "Struct Array Chunking Test" + }, + { + "result": { + "value": [ + [ + [ + [ + [ + [ + "abc" + ] + ] + ] + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 9,\n \"structType\": {\n \"fields\": [{\n \"name\": \"f11\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 9,\n \"structType\": {\n \"fields\": [{\n \"name\": \"f12\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n }\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [[[[[\"abc\"]]]]]\n}" + ], + "name": "Nested Struct Array Test" + }, + { + "result": { + "value": [ + [ + [ + [ + [ + [ + "abc" + ], + [ + "def" + ] + ] + ] + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 9,\n \"structType\": {\n \"fields\": [{\n \"name\": \"f11\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 9,\n \"structType\": {\n \"fields\": [{\n \"name\": \"f12\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n }\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [[[[[\"ab\"]]]]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[[[[\"c\"], [\"def\"]]]]]\n}" + ], + "name": "Nested Struct Array Chunking Test" + }, + { + "result": { + "value": [ + [ + "1", + [ + [ + "ab" + ] + ] + ], + [ + "2", + [ + [ + "c" + ] + ] + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 6\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": 8,\n \"arrayElementType\": {\n \"code\": 9,\n \"structType\": {\n \"fields\": [{\n \"name\": \"f21\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n }\n }\n }]\n }\n },\n \"values\": [\"1\", [[\"a\"]]],\n \"chunkedValue\": true\n}", + "{\n \"values\": [[[\"b\"]], \"2\"],\n \"chunkedValue\": true\n}", + "{\n \"values\": [\"\", [[\"c\"]]]\n}" + ], + "name": "Struct Array And String Chunking Test" + }, + { + "result": { + "value": [ + [ + "abc", + "1" + ], + [ + "def", + "2" + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 6\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": 2\n }\n }]\n }\n },\n \"values\": [\"abc\", \"1\", \"def\", \"2\"]\n}" + ], + "name": "Multiple Row Single Chunk" + }, + { + "result": { + "value": [ + [ + "abc", + "1" + ], + [ + "def", + "2" + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 6\n }\n }, {\n \"name\": \"f2\",\n \"type\": {\n \"code\": 2\n }\n }]\n }\n },\n \"values\": [\"ab\"],\n \"chunkedValue\": true\n}", + "{\n \"values\": [\"c\", \"1\", \"de\"],\n \"chunkedValue\": true\n}", + "{\n \"values\": [\"f\", \"2\"]\n}" + ], + "name": "Multiple Row Multiple Chunks" + }, + { + "result": { + "value": [ + [ + "ab" + ], + [ + "c" + ], + [ + "d" + ], + [ + "ef" + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n },\n \"values\": [\"a\"],\n \"chunkedValue\": true\n}", + "{\n \"values\": [\"b\", \"c\"]\n}", + "{\n \"values\": [\"d\", \"e\"],\n \"chunkedValue\": true\n}", + "{\n \"values\": [\"f\"]\n}" + ], + "name": "Multiple Row Chunks/Non Chunks Interleaved" + } + ] +} From 16292729ba88fdc883f7da5b3089919730ab5257 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 10 Apr 2017 10:02:05 -0400 Subject: [PATCH 07/46] Implement remaining API, add system tests. --- composer.json | 3 +- docs/contents/cloud-spanner.json | 42 +- docs/manifest.json | 8 + src/Core/Exception/AbortedException.php | 12 +- src/Core/Exception/ServiceException.php | 21 +- src/Core/GrpcRequestWrapper.php | 28 +- src/Core/GrpcTrait.php | 9 + src/Core/LongRunning/LROTrait.php | 94 ++- src/Core/LongRunning/LongRunningOperation.php | 10 +- .../LongRunning/OperationResponseTrait.php | 13 +- src/Core/PhpArray.php | 2 +- src/Logging/Connection/Grpc.php | 20 +- .../Connection/ConnectionInterface.php | 109 +-- src/Spanner/Connection/Grpc.php | 184 +++-- src/Spanner/Database.php | 455 ++++++---- src/Spanner/Date.php | 21 + src/Spanner/Instance.php | 167 ++-- ...guration.php => InstanceConfiguration.php} | 36 +- src/Spanner/KeyRange.php | 26 +- src/Spanner/KeySet.php | 30 +- src/Spanner/Operation.php | 74 +- src/Spanner/Result.php | 3 +- src/Spanner/Session/CacheSessionPool.php | 5 +- src/Spanner/Snapshot.php | 37 +- src/Spanner/SpannerClient.php | 96 +-- src/Spanner/Transaction.php | 71 +- src/Spanner/TransactionConfigurationTrait.php | 77 +- src/Spanner/TransactionalReadInterface.php | 67 ++ ...adTrait.php => TransactionalReadTrait.php} | 88 +- src/Spanner/V1/SpannerClient.php | 1 + src/Spanner/ValueMapper.php | 51 +- .../snippets/Language/LanguageClientTest.php | 2 +- tests/snippets/PubSub/PubSubClientTest.php | 4 +- tests/snippets/PubSub/SnapshotTest.php | 8 +- tests/snippets/PubSub/SubscriptionTest.php | 6 +- tests/snippets/ServiceBuilderTest.php | 10 +- tests/snippets/Spanner/BytesTest.php | 4 + tests/snippets/Spanner/DatabaseTest.php | 136 ++- tests/snippets/Spanner/DateTest.php | 13 + tests/snippets/Spanner/DurationTest.php | 4 + ...Test.php => InstanceConfigurationTest.php} | 39 +- tests/snippets/Spanner/InstanceTest.php | 90 +- tests/snippets/Spanner/KeyRangeTest.php | 23 + tests/snippets/Spanner/KeySetTest.php | 4 + tests/snippets/Spanner/ResultTest.php | 20 +- .../Spanner/Session/CacheSessionPoolTest.php | 42 + tests/snippets/Spanner/SnapshotTest.php | 10 +- tests/snippets/Spanner/SpannerClientTest.php | 44 +- tests/snippets/Spanner/TimestampTest.php | 4 + tests/snippets/Spanner/TransactionTest.php | 4 + tests/snippets/bootstrap.php | 4 +- tests/system/Spanner/AdminTest.php | 32 +- tests/system/Spanner/ConfigurationTest.php | 53 -- tests/system/Spanner/OperationsTest.php | 122 ++- tests/system/Spanner/SpannerTestCase.php | 12 +- tests/system/Spanner/TransactionTest.php | 87 +- tests/unit/Spanner/BytesTest.php | 2 +- tests/unit/Spanner/Connection/GrpcTest.php | 359 ++++++++ tests/unit/Spanner/DatabaseTest.php | 91 +- tests/unit/Spanner/DateTest.php | 8 +- tests/unit/Spanner/DurationTest.php | 2 +- ...Test.php => InstanceConfigurationTest.php} | 20 +- tests/unit/Spanner/InstanceTest.php | 48 +- tests/unit/Spanner/KeyRangeTest.php | 18 +- tests/unit/Spanner/KeySetTest.php | 20 +- tests/unit/Spanner/OperationTest.php | 16 +- tests/unit/Spanner/ResultTest.php | 68 +- tests/unit/Spanner/ResultTestTrait.php | 96 +++ .../Spanner/Session/CacheSessionPoolTest.php | 2 +- tests/unit/Spanner/SnapshotTest.php | 61 +- tests/unit/Spanner/SpannerClientTest.php | 56 +- tests/unit/Spanner/TimestampTest.php | 2 +- .../TransactionConfigurationTraitTest.php | 17 +- tests/unit/Spanner/TransactionTest.php | 22 +- tests/unit/Spanner/TransactionTypeTest.php | 777 ++++++++++++++++++ tests/unit/Spanner/ValueMapperTest.php | 6 +- 76 files changed, 3439 insertions(+), 889 deletions(-) rename src/Spanner/{Configuration.php => InstanceConfiguration.php} (82%) create mode 100644 src/Spanner/TransactionalReadInterface.php rename src/Spanner/{TransactionReadTrait.php => TransactionalReadTrait.php} (56%) rename tests/snippets/Spanner/{ConfigurationTest.php => InstanceConfigurationTest.php} (62%) create mode 100644 tests/snippets/Spanner/Session/CacheSessionPoolTest.php delete mode 100644 tests/system/Spanner/ConfigurationTest.php create mode 100644 tests/unit/Spanner/Connection/GrpcTest.php rename tests/unit/Spanner/{ConfigurationTest.php => InstanceConfigurationTest.php} (79%) create mode 100644 tests/unit/Spanner/ResultTestTrait.php create mode 100644 tests/unit/Spanner/TransactionTypeTest.php diff --git a/composer.json b/composer.json index 8b061ebcd98f..aaff7d30aaa7 100644 --- a/composer.json +++ b/composer.json @@ -72,7 +72,8 @@ "autoload-dev": { "psr-4": { "Google\\Cloud\\Dev\\": "dev/src", - "Google\\Cloud\\Tests\\System\\": "tests/system" + "Google\\Cloud\\Tests\\System\\": "tests/system", + "Google\\Cloud\\Tests\\Unit\\": "tests/unit" }, "files": ["dev/src/Functions.php"] }, diff --git a/docs/contents/cloud-spanner.json b/docs/contents/cloud-spanner.json index 191f68771ba9..f3459b3c5aa0 100644 --- a/docs/contents/cloud-spanner.json +++ b/docs/contents/cloud-spanner.json @@ -1,8 +1,44 @@ { "title": "Spanner", "pattern": "spanner\/\\w{1,}", - "nav": [{ - "title": "SpannerClient", - "type": "spanner/spannerclient" + "services": [{ + "title": "SpannerClient", + "type": "spanner/spannerclient" + }, { + "title": "Configuration", + "type": "spanner/configuration" + }, { + "title": "Database", + "type": "spanner/database" + }, { + "title": "Instance", + "type": "spanner/instance" + }, { + "title": "Snapshot", + "type": "spanner/snapshot" + }, { + "title": "Transaction", + "type": "spanner/transaction" + }, { + "title": "Bytes", + "type": "spanner/bytes" + }, { + "title": "Date", + "type": "spanner/date" + }, { + "title": "Duration", + "type": "spanner/duration" + }, { + "title": "KeyRange", + "type": "spanner/keyrange" + }, { + "title": "KeySet", + "type": "spanner/keyset" + }, { + "title": "Result", + "type": "spanner/result" + }, { + "title": "Timestamp", + "type": "spanner/timestamp" }] } diff --git a/docs/manifest.json b/docs/manifest.json index 811e7c8ef55c..14dbd2124e2a 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -132,6 +132,14 @@ "master" ] }, + { + "id": "cloud-spanner", + "name": "google/cloud-spanner", + "defaultService": "spanner/spannerclient", + "versions": [ + "master" + ] + }, { "id": "cloud-speech", "name": "google/cloud-speech", diff --git a/src/Core/Exception/AbortedException.php b/src/Core/Exception/AbortedException.php index 60cd19e02130..842088b1fb00 100644 --- a/src/Core/Exception/AbortedException.php +++ b/src/Core/Exception/AbortedException.php @@ -29,7 +29,7 @@ class AbortedException extends ServiceException */ public function getRetryDelay() { - $metadata = array_filter($this->options, function ($metadataItem) { + $metadata = array_filter($this->metadata, function ($metadataItem) { if (array_key_exists('retryDelay', $metadataItem)) { return true; } @@ -37,11 +37,13 @@ public function getRetryDelay() return false; }); - $delay = $metadata[0]['retryDelay']; - if (!isset($delay['seconds'])) { - $delay['seconds'] = 0; + if (count($metadata) === 0) { + return ['seconds' => 0, 'nanos' => 0]; } - return $delay; + return $metadata[0]['retryDelay'] + [ + 'seconds' => 0, + 'nanos' => 0 + ]; } } diff --git a/src/Core/Exception/ServiceException.php b/src/Core/Exception/ServiceException.php index 07838cf6a789..0efc81ec2caa 100644 --- a/src/Core/Exception/ServiceException.php +++ b/src/Core/Exception/ServiceException.php @@ -32,23 +32,24 @@ class ServiceException extends GoogleException /** * @var array */ - protected $options; + protected $metadata; /** * Handle previous exceptions differently here. * - * @param string $message - * @param int $code - * @param Exception $serviceException + * @param string $message + * @param int $code + * @param Exception $serviceException + * @param array $metadata [optional] Exception metadata. */ public function __construct( $message, $code = null, Exception $serviceException = null, - array $options = [] + array $metadata = [] ) { $this->serviceException = $serviceException; - $this->options = $options; + $this->metadata = $metadata; parent::__construct($message, $code); } @@ -72,4 +73,12 @@ public function getServiceException() { return $this->serviceException; } + + /** + * Get exception metadata. + */ + public function getMetadata() + { + return $this->metadata; + } } diff --git a/src/Core/GrpcRequestWrapper.php b/src/Core/GrpcRequestWrapper.php index e8fcd2fdb98f..ffe5066121ac 100644 --- a/src/Core/GrpcRequestWrapper.php +++ b/src/Core/GrpcRequestWrapper.php @@ -75,7 +75,8 @@ class GrpcRequestWrapper * @var array Map of error metadata types to RPC wrappers. */ private $metadataTypes = [ - 'google.rpc.retryinfo-bin' => \google\rpc\RetryInfo::class + 'google.rpc.retryinfo-bin' => \google\rpc\RetryInfo::class, + 'google.rpc.badrequest-bin' => \google\rpc\BadRequest::class ]; /** @@ -168,7 +169,8 @@ private function handleResponse($response) } if ($response instanceof Message) { - return $response->serialize($this->codec); + $res = $response->serialize($this->codec); + return $this->convertNulls($res); } if ($response instanceof OperationResponse) { @@ -192,7 +194,8 @@ private function handleStream(ServerStream $response) { try { foreach ($response->readAll() as $count => $result) { - yield $result->serialize($this->codec); + $res = $result->serialize($this->codec); + yield $this->convertNulls($res); } } catch (\Exception $ex) { throw $this->convertToGoogleException($ex); @@ -256,4 +259,23 @@ private function convertToGoogleException(ApiException $ex) return new $exception($ex->getMessage(), $ex->getCode(), $ex, $metadata); } + + /** + * Convert NullValue types to PHP null. + * + * @param array [ref] $result + * @return array + */ + private function convertNulls(array &$result) + { + foreach ($result as $key => $value) { + if (is_array($value) && array_key_exists('nullValue', $value) && $value['nullValue'][0] === null) { + $result[$key] = null; + } elseif (is_array($value)) { + $result[$key] = $this->convertNulls($value); + } + } + + return $result; + } } diff --git a/src/Core/GrpcTrait.php b/src/Core/GrpcTrait.php index 4fa5de20aa15..9867f26d8803 100644 --- a/src/Core/GrpcTrait.php +++ b/src/Core/GrpcTrait.php @@ -23,6 +23,7 @@ use Google\Auth\Cache\MemoryCacheItemPool; use Google\Cloud\Core\ArrayTrait; use Google\Cloud\Core\GrpcRequestWrapper; +use google\protobuf; /** * Provides shared functionality for gRPC service implementations. @@ -88,10 +89,16 @@ private function getGaxConfig($version) */ private function formatTimestampFromApi(array $timestamp) { + $timestamp += [ + 'seconds' => 0, + 'nanos' => 0 + ]; + $formattedTime = (new DateTime()) ->setTimeZone(new DateTimeZone('UTC')) ->setTimestamp($timestamp['seconds']) ->format('Y-m-d\TH:i:s'); + $timestamp['nanos'] = str_pad($timestamp['nanos'], 9, '0', STR_PAD_LEFT); return $formattedTime .= sprintf('.%sZ', rtrim($timestamp['nanos'], '0')); } @@ -171,6 +178,8 @@ private function formatValueForApi($value) return ['number_value' => $value]; case 'boolean': return ['bool_value' => $value]; + case 'NULL': + return ['null_value' => protobuf\NullValue::NULL_VALUE]; case 'array': if ($this->isAssoc($value)) { return ['struct_value' => $this->formatStructForApi($value)]; diff --git a/src/Core/LongRunning/LROTrait.php b/src/Core/LongRunning/LROTrait.php index 0b965357231e..2308054a343e 100644 --- a/src/Core/LongRunning/LROTrait.php +++ b/src/Core/LongRunning/LROTrait.php @@ -17,6 +17,8 @@ namespace Google\Cloud\Core\LongRunning; +use Google\Cloud\Core\Iterator\ItemIterator; +use Google\Cloud\Core\Iterator\PageIterator; use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; /** @@ -27,19 +29,97 @@ trait LROTrait { /** - * Create a Long Running Operation from an operation name. + * @var LongRunningConnectionInterface + */ + private $lroConnection; + + /** + * @var array + */ + private $lroCallables; + + /** + * @var string + */ + private $lroResource; + + /** + * Populate required LRO properties. * - * @param LongRunningConnectionInterface $connection The LRO connection - * @param string $operationName The name of the Operation. - * @param array $lroCallables A map of callables to normalize inputs and results. + * @param LongRunningConnectionInterface $lroConnection The LRO Connection. + * @param array $callablesMap An collection of form [(string) typeUrl, (callable) callable] + * providing a function to invoke when an operation completes. The + * callable Type should correspond to an expected value of + * operation.metadata.typeUrl. + * @param string $lroResource [optional] The resource for which operations + * may be listed. + */ + private function setLroProperties( + LongRunningConnectionInterface $lroConnection, + array $lroCallables, + $resource = null + ) { + $this->lroConnection = $lroConnection; + $this->lroCallables = $lroCallables; + $this->lroResource = $resource; + } + + /** + * Resume a Long Running Operation + * + * @param string $operationName The Long Running Operation name. + * @param array $info [optional] The operation data. * @return LongRunningOperation */ - private function lro(LongRunningConnectionInterface $connection, $operationName, array $lroCallables) + public function resumeOperation($operationName, array $info = []) { return new LongRunningOperation( - $connection, + $this->lroConnection, $operationName, - $lroCallables + $this->lroCallables, + $info + ); + } + + /** + * List long running operations. + * + * @param array $options [optional] { + * Configuration Options. + * + * @type string $name The name of the operation collection. + * @type string $filter The standard list filter. + * @type int $pageSize Maximum number of results to return per + * request. + * @type int $resultLimit Limit the number of results returned in total. + * **Defaults to** `0` (return all results). + * @type string $pageToken A previously-returned page token used to + * resume the loading of results from a specific point. + * } + * @return ItemIterator + */ + public function longRunningOperations(array $options = []) + { + if (is_null($this->lroResource)) { + throw new \BadMethodCallException('This service does list support listing operations.'); + } + + $resultLimit = $this->pluck('resultLimit', $options, false) ?: 0; + + $options['name'] = $this->lroResource .'/operations'; + + return new ItemIterator( + new PageIterator( + function (array $operation) { + return $this->resumeOperation($operation['name'], $operation); + }, + [$this->lroConnection, 'operations'], + $options, + [ + 'itemsKey' => 'operations', + 'resultLimit' => $resultLimit + ] + ) ); } } diff --git a/src/Core/LongRunning/LongRunningOperation.php b/src/Core/LongRunning/LongRunningOperation.php index 5828793f20c9..63732d91ba53 100644 --- a/src/Core/LongRunning/LongRunningOperation.php +++ b/src/Core/LongRunning/LongRunningOperation.php @@ -41,7 +41,7 @@ class LongRunningOperation /** * @var array */ - private $info; + private $info = []; /** * @var array|null @@ -66,15 +66,18 @@ class LongRunningOperation * providing a function to invoke when an operation completes. The * callable Type should correspond to an expected value of * operation.metadata.typeUrl. + * @param array $info [optional] The operation info. */ public function __construct( LongRunningConnectionInterface $connection, $name, - array $callablesMap + array $callablesMap, + array $info = [] ) { $this->connection = $connection; $this->name = $name; $this->callablesMap = $callablesMap; + $this->info = $info; } /** @@ -371,7 +374,8 @@ public function __debugInfo() return [ 'connection' => get_class($this->connection), 'name' => $this->name, - 'callablesMap' => array_keys($this->callablesMap) + 'callablesMap' => array_keys($this->callablesMap), + 'info' => $this->info ]; } } diff --git a/src/Core/LongRunning/OperationResponseTrait.php b/src/Core/LongRunning/OperationResponseTrait.php index 233f7a2122c7..7db8dfaf8855 100644 --- a/src/Core/LongRunning/OperationResponseTrait.php +++ b/src/Core/LongRunning/OperationResponseTrait.php @@ -27,6 +27,14 @@ */ trait OperationResponseTrait { + /** + * Convert a GAX OperationResponse object to an array. + * + * @param OperationResponse $operation The operation response + * @param CodecInterface $codec The codec to use for gRPC serialization/deserialization. + * @param array $lroMappers A list of mappers for deserializing operation results. + * @return array + */ private function operationToArray(OperationResponse $operation, CodecInterface $codec, array $lroMappers) { $response = $operation->getLastProtoResponse(); @@ -58,11 +66,12 @@ private function operationToArray(OperationResponse $operation, CodecInterface $ * * @param mixed $client A generated client with a `resumeOperation` method. * @param string $name The Operation name. + * @param string|null $method The method name. * @return OperationResponse */ - private function getOperationByName($client, $name) + private function getOperationByName($client, $name, $method = null) { - return $client->resumeOperation($name); + return $client->resumeOperation($name, $method); } /** diff --git a/src/Core/PhpArray.php b/src/Core/PhpArray.php index 7829afbce919..cf8a5fd360b7 100644 --- a/src/Core/PhpArray.php +++ b/src/Core/PhpArray.php @@ -159,7 +159,7 @@ protected function decodeMessage(Protobuf\Message $message, $data) protected function filterValue($value, Protobuf\Field $field) { if (trim($field->getReference(), '\\') === NullValue::class) { - return null; + return 0; } if ($value instanceof Protobuf\Message) { diff --git a/src/Logging/Connection/Grpc.php b/src/Logging/Connection/Grpc.php index cfa006bbb0f4..b5f00f8aeb94 100644 --- a/src/Logging/Connection/Grpc.php +++ b/src/Logging/Connection/Grpc.php @@ -90,17 +90,15 @@ class Grpc implements ConnectionInterface public function __construct(array $config = []) { $this->codec = new PhpArray([ - 'customFilters' => [ - 'timestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - }, - 'severity' => function ($v) { - return Logger::getLogLevelMap()[$v]; - }, - 'outputVersionFormat' => function ($v) { - return self::$versionFormatMap[$v]; - } - ] + 'timestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + }, + 'severity' => function ($v) { + return Logger::getLogLevelMap()[$v]; + }, + 'outputVersionFormat' => function ($v) { + return self::$versionFormatMap[$v]; + } ]); $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); diff --git a/src/Spanner/Connection/ConnectionInterface.php b/src/Spanner/Connection/ConnectionInterface.php index 12de8323345a..cb6e1d5501ce 100644 --- a/src/Spanner/Connection/ConnectionInterface.php +++ b/src/Spanner/Connection/ConnectionInterface.php @@ -23,136 +23,141 @@ interface ConnectionInterface { /** - * @param array $args [optional] + * @param array $args */ - public function listConfigs(array $args = []); + public function listInstanceConfigs(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function getConfig(array $args = []); + public function getInstanceConfig(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function listInstances(array $args = []); + public function listInstances(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function getInstance(array $args = []); + public function getInstance(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function createInstance(array $args = []); + public function createInstance(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function updateInstance(array $args = []); + public function updateInstance(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function deleteInstance(array $args = []); + public function deleteInstance(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function getInstanceIamPolicy(array $args = []); + public function getInstanceIamPolicy(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function setInstanceIamPolicy(array $args = []); + public function setInstanceIamPolicy(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function testInstanceIamPermissions(array $args = []); + public function testInstanceIamPermissions(array $args); /** - * @param array $args [optional] + * @param array $args + */ + public function listDatabases(array $args); + + /** + * @param array $args */ - public function listDatabases(array $args = []); + public function createDatabase(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function createDatabase(array $args = []); + public function updateDatabaseDdl(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function updateDatabase(array $args = []); + public function dropDatabase(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function dropDatabase(array $args = []); + public function getDatabase(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function getDatabaseDDL(array $args = []); + public function getDatabaseDDL(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function getDatabaseIamPolicy(array $args = []); + public function getDatabaseIamPolicy(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function setDatabaseIamPolicy(array $args = []); + public function setDatabaseIamPolicy(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function testDatabaseIamPermissions(array $args = []); + public function testDatabaseIamPermissions(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function createSession(array $args = []); + public function createSession(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function getSession(array $args = []); + public function getSession(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function deleteSession(array $args = []); + public function deleteSession(array $args); /** - * @param array $args [optional] + * @param array $args * @return \Generator */ - public function executeStreamingSql(array $args = []); + public function executeStreamingSql(array $args); /** - * @param array $args [optional] + * @param array $args * @return \Generator */ - public function streamingRead(array $args = []); + public function streamingRead(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function beginTransaction(array $args = []); + public function beginTransaction(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function commit(array $args = []); + public function commit(array $args); /** - * @param array $args [optional] + * @param array $args */ - public function rollback(array $args = []); + public function rollback(array $args); /** * @param array $args diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 532ea335599a..a02d4f179364 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -24,6 +24,7 @@ use Google\Cloud\Core\PhpArray; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\SpannerClient as VeneerSpannerClient; use Google\Cloud\Spanner\V1\SpannerClient; use Google\GAX\ApiException; @@ -116,14 +117,12 @@ class Grpc implements ConnectionInterface public function __construct(array $config = []) { $this->codec = new PhpArray([ - 'customFilters' => [ - 'commitTimestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - }, - 'readTimestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - } - ] + 'commitTimestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + }, + 'readTimestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + } ]); $config['codec'] = $this->codec; @@ -141,9 +140,9 @@ public function __construct(array $config = []) } /** - * @param array $args [optional] + * @param array $args */ - public function listConfigs(array $args = []) + public function listInstanceConfigs(array $args) { return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ $this->pluck('projectId', $args), @@ -152,9 +151,9 @@ public function listConfigs(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function getConfig(array $args = []) + public function getInstanceConfig(array $args) { return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ $this->pluck('name', $args), @@ -163,9 +162,9 @@ public function getConfig(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function listInstances(array $args = []) + public function listInstances(array $args) { return $this->send([$this->instanceAdminClient, 'listInstances'], [ $this->pluck('projectId', $args), @@ -174,9 +173,9 @@ public function listInstances(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function getInstance(array $args = []) + public function getInstance(array $args) { return $this->send([$this->instanceAdminClient, 'getInstance'], [ $this->pluck('name', $args), @@ -185,9 +184,9 @@ public function getInstance(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function createInstance(array $args = []) + public function createInstance(array $args) { $instance = $this->instanceObject($args, true); $res = $this->send([$this->instanceAdminClient, 'createInstance'], [ @@ -201,13 +200,13 @@ public function createInstance(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function updateInstance(array $args = []) + public function updateInstance(array $args) { $instanceObject = $this->instanceObject($args); - $mask = array_keys($instanceObject->serialize(new PhpArray(['useCamelCase' => false]))); + $mask = array_keys($instanceObject->serialize(new PhpArray([], false))); $fieldMask = (new protobuf\FieldMask())->deserialize(['paths' => $mask], $this->codec); @@ -221,9 +220,9 @@ public function updateInstance(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function deleteInstance(array $args = []) + public function deleteInstance(array $args) { return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ $this->pluck('name', $args), @@ -232,9 +231,9 @@ public function deleteInstance(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function getInstanceIamPolicy(array $args = []) + public function getInstanceIamPolicy(array $args) { return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ $this->pluck('resource', $args), @@ -243,9 +242,9 @@ public function getInstanceIamPolicy(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function setInstanceIamPolicy(array $args = []) + public function setInstanceIamPolicy(array $args) { return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ $this->pluck('resource', $args), @@ -255,9 +254,9 @@ public function setInstanceIamPolicy(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function testInstanceIamPermissions(array $args = []) + public function testInstanceIamPermissions(array $args) { return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ $this->pluck('resource', $args), @@ -267,9 +266,9 @@ public function testInstanceIamPermissions(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function listDatabases(array $args = []) + public function listDatabases(array $args) { return $this->send([$this->databaseAdminClient, 'listDatabases'], [ $this->pluck('instance', $args), @@ -278,9 +277,9 @@ public function listDatabases(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function createDatabase(array $args = []) + public function createDatabase(array $args) { $res = $this->send([$this->databaseAdminClient, 'createDatabase'], [ $this->pluck('instance', $args), @@ -293,9 +292,9 @@ public function createDatabase(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function updateDatabase(array $args = []) + public function updateDatabaseDdl(array $args) { $res = $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [ $this->pluck('name', $args), @@ -307,9 +306,9 @@ public function updateDatabase(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function dropDatabase(array $args = []) + public function dropDatabase(array $args) { return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ $this->pluck('name', $args), @@ -318,9 +317,20 @@ public function dropDatabase(array $args = []) } /** - * @param array $args [optional] + * @param array $args + */ + public function getDatabase(array $args) + { + return $this->send([$this->databaseAdminClient, 'getDatabase'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args */ - public function getDatabaseDDL(array $args = []) + public function getDatabaseDDL(array $args) { return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ $this->pluck('name', $args), @@ -329,9 +339,9 @@ public function getDatabaseDDL(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function getDatabaseIamPolicy(array $args = []) + public function getDatabaseIamPolicy(array $args) { return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ $this->pluck('resource', $args), @@ -340,9 +350,9 @@ public function getDatabaseIamPolicy(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function setDatabaseIamPolicy(array $args = []) + public function setDatabaseIamPolicy(array $args) { return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ $this->pluck('resource', $args), @@ -352,9 +362,9 @@ public function setDatabaseIamPolicy(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function testDatabaseIamPermissions(array $args = []) + public function testDatabaseIamPermissions(array $args) { return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ $this->pluck('resource', $args), @@ -364,9 +374,9 @@ public function testDatabaseIamPermissions(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function createSession(array $args = []) + public function createSession(array $args) { return $this->send([$this->spannerClient, 'createSession'], [ $this->pluck('database', $args), @@ -375,9 +385,9 @@ public function createSession(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function getSession(array $args = []) + public function getSession(array $args) { return $this->send([$this->spannerClient, 'getSession'], [ $this->pluck('name', $args), @@ -386,9 +396,9 @@ public function getSession(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function deleteSession(array $args = []) + public function deleteSession(array $args) { return $this->send([$this->spannerClient, 'deleteSession'], [ $this->pluck('name', $args), @@ -397,20 +407,24 @@ public function deleteSession(array $args = []) } /** - * @param array $args [optional] + * @param array $args * @return \Generator */ - public function executeStreamingSql(array $args = []) + public function executeStreamingSql(array $args) { $params = $this->pluck('params', $args); if ($params) { + // print_r($this->formatStructForApi($params)); $args['params'] = (new protobuf\Struct) ->deserialize($this->formatStructForApi($params), $this->codec); + // var_dump($args['params']);exit; } - foreach ($args['paramTypes'] as $key => $param) { - $args['paramTypes'][$key] = (new Type) - ->deserialize($param, $this->codec); + if (isset($args['paramTypes']) && is_array($args['paramTypes'])) { + foreach ($args['paramTypes'] as $key => $param) { + $args['paramTypes'][$key] = (new Type) + ->deserialize($param, $this->codec); + } } $args['transaction'] = $this->createTransactionSelector($args); @@ -423,10 +437,10 @@ public function executeStreamingSql(array $args = []) } /** - * @param array $args [optional] + * @param array $args * @return \Generator */ - public function streamingRead(array $args = []) + public function streamingRead(array $args) { $keySet = $this->pluck('keySet', $args); $keySet = (new KeySet) @@ -444,15 +458,15 @@ public function streamingRead(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function beginTransaction(array $args = []) + public function beginTransaction(array $args) { $options = new TransactionOptions; - if (isset($args['transactionOptions']['readOnly'])) { - $ro = $args['transactionOptions']['readOnly']; - + $transactionOptions = $this->pluck('transactionOptions', $args); + if (isset($transactionOptions['readOnly'])) { + $ro = $transactionOptions['readOnly']; if (isset($ro['minReadTimestamp'])) { $ro['minReadTimestamp'] = $this->formatTimestampForApi($ro['minReadTimestamp']); } @@ -478,9 +492,9 @@ public function beginTransaction(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function commit(array $args = []) + public function commit(array $args) { $inputMutations = $this->pluck('mutations', $args); @@ -491,7 +505,7 @@ public function commit(array $args = []) $data = $mutation[$type]; switch ($type) { - case 'delete': + case Operation::OP_DELETE: if (isset($data['keySet'])) { $data['keySet'] = $this->formatKeySet($data['keySet']); } @@ -533,9 +547,9 @@ public function commit(array $args = []) } /** - * @param array $args [optional] + * @param array $args */ - public function rollback(array $args = []) + public function rollback(array $args) { return $this->send([$this->spannerClient, 'rollback'], [ $this->pluck('session', $args), @@ -564,7 +578,10 @@ public function cancelOperation(array $args) $name = $this->pluck('name', $args); $method = $this->pluck('method', $args); - $operation = $this->getOperationByNameAndMethod($name, $method); + $operation = $this->getOperationByName($this->databaseAdminClient, $name, $method); + $operation->cancel(); + + return $this->operationToArray($operation, $this->codec, $this->lroResponseMappers); } /** @@ -575,7 +592,10 @@ public function deleteOperation(array $args) $name = $this->pluck('name', $args); $method = $this->pluck('method', $args); - $operation = $this->getOperationByNameAndMethod($name, $method); + $operation = $this->getOperationByName($this->databaseAdminClient, $name, $method); + $operation->delete(); + + return $this->operationToArray($operation, $this->codec, $this->lroResponseMappers); } /** @@ -583,8 +603,16 @@ public function deleteOperation(array $args) */ public function listOperations(array $args) { - $name = $this->pluck('name', $args); - $method = $this->pluck('method', $args); + $name = $this->pluck('name', $args, false) ?: ''; + $filter = $this->pluck('filter', $args, false) ?: ''; + + $client = $this->databaseAdminClient->getOperationsClient(); + + return $this->send([$client, 'listOperations'], [ + $name, + $filter, + $args + ]); } /** @@ -593,8 +621,9 @@ public function listOperations(array $args) */ private function formatKeySet(array $keySet) { - if (isset($keySet['keys'])) { - $keySet['keys'] = $this->formatListForApi($keySet['keys']); + $keys = $this->pluck('keys', $keySet, false); + if ($keys) { + $keySet['keys'] = $this->formatListForApi($keys); } if (isset($keySet['ranges'])) { @@ -636,13 +665,18 @@ private function createTransactionSelector(array &$args) */ private function instanceObject(array &$args, $required = false) { + $labels = null; + if (isset($args['labels'])) { + $labels = $this->formatLabelsForApi($this->pluck('labels', $args, $required)); + } + return (new Instance())->deserialize(array_filter([ 'name' => $this->pluck('name', $args, $required), 'config' => $this->pluck('config', $args, $required), 'displayName' => $this->pluck('displayName', $args, $required), 'nodeCount' => $this->pluck('nodeCount', $args, $required), 'state' => $this->pluck('state', $args, $required), - 'labels' => $this->formatLabelsForApi($this->pluck('labels', $args, $required)) + 'labels' => $labels ]), $this->codec); } } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 27a5f477fad3..e0099b7bb129 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -20,18 +20,22 @@ use Google\Cloud\Core\Exception\AbortedException; use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\Iam\Iam; -use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\LongRunning\LROTrait; use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Retry; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\Transaction; use Google\Cloud\Spanner\V1\SpannerClient as GrpcSpannerClient; +use Google\GAX\ValidationException; /** - * Represents a Google Cloud Spanner Database. + * Represents a Cloud Spanner Database. * * Example: * ``` @@ -51,6 +55,41 @@ * $instance = $spanner->instance('my-instance'); * $database = $instance->database('my-database'); * ``` + * + * @method resumeOperation() { + * Resume a Long Running Operation + * + * Example: + * ``` + * $operation = $database->resumeOperation($operationName); + * ``` + * + * @param string $operationName The Long Running Operation name. + * @param array $info [optional] The operation data. + * @return LongRunningOperation + * } + * @method longRunningOperations() { + * List long running operations. + * + * Example: + * ``` + * $operations = $database->longRunningOperations(); + * ``` + * + * @param array $options [optional] { + * Configuration Options. + * + * @type string $name The name of the operation collection. + * @type string $filter The standard list filter. + * @type int $pageSize Maximum number of results to return per + * request. + * @type int $resultLimit Limit the number of results returned in total. + * **Defaults to** `0` (return all results). + * @type string $pageToken A previously-returned page token used to + * resume the loading of results from a specific point. + * } + * @return ItemIterator + * } */ class Database { @@ -69,11 +108,6 @@ class Database */ private $instance; - /** - * @var LongRunningConnectionInterface - */ - private $lroConnection; - /** * @var Operation */ @@ -89,6 +123,11 @@ class Database */ private $name; + /** + * @var array + */ + private $info; + /** * @var Iam */ @@ -108,12 +147,12 @@ class Database * Create an object representing a Database. * * @param ConnectionInterface $connection The connection to the - * Google Cloud Spanner Admin API. + * Cloud Spanner Admin API. * @param Instance $instance The instance in which the database exists. * @param LongRunningConnectionInterface $lroConnection An implementation * mapping to methods which handle LRO resolution in the service. * @param string $projectId The project ID. - * @param string $name The database name. + * @param string $name The database name or ID. * @param SessionPoolInterface $sessionPool [optional] The session pool * implementation. * @param bool $returnInt64AsObject [optional If true, 64 bit integers will @@ -132,20 +171,20 @@ public function __construct( ) { $this->connection = $connection; $this->instance = $instance; - $this->lroConnection = $lroConnection; - $this->lroCallables = $lroCallables; $this->projectId = $projectId; - $this->name = $name; + $this->name = $this->fullyQualifiedDatabaseName($name); $this->sessionPool = $sessionPool; $this->operation = new Operation($connection, $returnInt64AsObject); if ($this->sessionPool) { $this->sessionPool->setDatabase($this); } + + $this->setLroProperties($lroConnection, $lroCallables, $this->name); } /** - * Return the simple database name. + * Return the fully-qualified database name. * * Example: * ``` @@ -159,6 +198,48 @@ public function name() return $this->name; } + /** + * Get the database info + * + * Example: + * ``` + * $info = $database->info(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.Database Database + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function info(array $options = []) + { + return $this->info ?: $this->reload($options); + } + + /** + * Reload the database info from the Cloud Spanner API. + * + * Example: + * ``` + * $info = $database->reload(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.Database Database + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function reload(array $options = []) + { + return $this->info = $this->connection->getDatabase([ + 'name' => $this->name + ] + $options); + } + /** * Check if the database exists. * @@ -179,7 +260,7 @@ public function name() public function exists(array $options = []) { try { - $this->ddl($options); + $this->reload($options); } catch (NotFoundException $e) { return false; } @@ -187,6 +268,42 @@ public function exists(array $options = []) return true; } + /** + * Create a new Cloud Spanner database. + * + * Example: + * ``` + * $operation = $database->create(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#createdatabaserequest CreateDatabaseRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] { + * Configuration Options + * + * @type array $statements Additional DDL statements. + * } + * @return LongRunningOperation + */ + public function create(array $options = []) + { + $options += [ + 'statements' => [], + ]; + + $statement = sprintf('CREATE DATABASE `%s`', DatabaseAdminClient::parseDatabaseFromDatabaseName($this->name)); + + $operation = $this->connection->createDatabase([ + 'instance' => $this->instance->name(), + 'createStatement' => $statement, + 'extraStatements' => $options['statements'] + ]); + + return $this->resumeOperation($operation['name'], $operation); + } + /** * Update the Database schema by running a SQL statement. * @@ -249,12 +366,12 @@ public function updateDdl($statement, array $options = []) */ public function updateDdlBatch(array $statements, array $options = []) { - $operation = $this->connection->updateDatabase($options + [ - 'name' => $this->fullyQualifiedDatabaseName(), + $operation = $this->connection->updateDatabaseDdl($options + [ + 'name' => $this->name, 'statements' => $statements, ]); - return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables); + return $this->resumeOperation($operation['name'], $operation); } /** @@ -277,7 +394,7 @@ public function updateDdlBatch(array $statements, array $options = []) public function drop(array $options = []) { $this->connection->dropDatabase($options + [ - 'name' => $this->fullyQualifiedDatabaseName() + 'name' => $this->name ]); } @@ -301,7 +418,7 @@ public function drop(array $options = []) public function ddl(array $options = []) { $ddl = $this->connection->getDatabaseDDL($options + [ - 'name' => $this->fullyQualifiedDatabaseName() + 'name' => $this->name ]); if (isset($ddl['statements'])) { @@ -326,7 +443,7 @@ public function iam() if (!$this->iam) { $this->iam = new Iam( new IamDatabase($this->connection), - $this->fullyQualifiedDatabaseName() + $this->name ); } @@ -377,25 +494,82 @@ public function iam() * timestamp. * @type Duration $exactStaleness Represents a number of seconds. Executes * all reads at a timestamp that is $exactStaleness old. + * @type Timestamp $minReadTimestamp Executes all reads at a + * timestamp >= min_read_timestamp. Only available when + * `$options.singleUse` is true. + * @type Duration $maxStaleness Read data at a timestamp >= NOW - max_staleness + * seconds. Guarantees that all writes that have committed more + * than the specified number of seconds ago are visible. Only + * available when `$options.singleUse` is true. + * @type bool $singleUse If true, a Transaction ID will not be allocated + * up front. Instead, the transaction will be considered + * "single-use", and may be used for only a single operation. + * **Defaults to** `false`. * } * @return Snapshot * @codingStandardsIgnoreEnd */ public function snapshot(array $options = []) { - // These are only available in single-use transactions. - if (isset($options['maxStaleness']) || isset($options['minReadTimestamp'])) { - throw new \BadMethodCallException( - 'maxStaleness and minReadTimestamp are only available in single-use transactions.' - ); - } + $options += [ + 'singleUse' => false + ]; - $transactionOptions = $this->configureSnapshotOptions($options); + $options['transactionOptions'] = $this->configureSnapshotOptions($options); $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); try { - return $this->operation->snapshot($session, $transactionOptions); + return $this->operation->snapshot($session, $options); + } finally { + $session->setExpiration(); + } + } + + /** + * Create and return a new read/write Transaction. + * + * When manually using a Transaction, it is advised that retry logic be + * implemented to reapply all operations when an instance of + * {@see Google\Cloud\Core\Exception\AbortedException} is thrown. + * + * If you wish Google Cloud PHP to handle retry logic for you (recommended + * for most cases), use {@see Google\Cloud\Spanner\Database::runTransaction()}. + * + * Please note that once a transaction reads data, it will lock the read + * data, preventing other users from modifying that data. For this reason, + * it is important that every transaction commits or rolls back as early as + * possible. Do not hold transactions open longer than necessary. + * + * Example: + * ``` + * $transaction = $database->transaction(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] { + * Configuration Options. + * + * @type bool $singleUse If true, a Transaction ID will not be allocated + * up front. Instead, the transaction will be considered + * "single-use", and may be used for only a single operation. + * **Defaults to** `false`. + * } + * @return Transaction + */ + public function transaction(array $options = []) + { + // There isn't anything configurable here. + $options['transactionOptions'] = $this->configureTransactionOptions(); + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + try { + return $this->operation->transaction($session, $options); } finally { $session->setExpiration(); } @@ -419,11 +593,10 @@ public function snapshot(array $options = []) * it is important that every transaction commits or rolls back as early as * possible. Do not hold transactions open longer than necessary. * - * If you have an active transaction which was obtained from elsewhere, you - * can provide it to this method and gain the benefits of managed retry by - * setting `$options.transaction` to your {@see Google\Cloud\Spanner\Transaction} - * instance. Please note that in this case, it is important that ALL reads - * and mutations MUST be performed within the runTransaction callable. + * If a callable finishes executing without invoking + * {@see Google\Cloud\Spanner\Transaction::commit()} or + * {@see Google\Cloud\Spanner\Transaction::rollback()}, the transaction will + * automatically be rolled back and `RuntimeException` thrown. * * Example: * ``` @@ -462,17 +635,20 @@ public function snapshot(array $options = []) * * @type int $maxRetries The number of times to attempt to apply the * operation before failing. **Defaults to ** `3`. - * @type Transaction $transaction If provided, the transaction will be - * passed to the callable instead of attempting to begin a new - * transaction. + * @type bool $singleUse If true, a Transaction ID will not be allocated + * up front. Instead, the transaction will be considered + * "single-use", and may be used for only a single operation. Note + * that in a single-use transaction, only a single operation may + * be executed, and rollback is not available. **Defaults to** + * `false`. * } * @return mixed The return value of `$operation`. + * @throws RuntimeException */ public function runTransaction(callable $operation, array $options = []) { $options += [ 'maxRetries' => self::MAX_RETRIES, - 'transaction' => null ]; // There isn't anything configurable here. @@ -482,13 +658,7 @@ public function runTransaction(callable $operation, array $options = []) $attempt = 0; $startTransactionFn = function ($session, $options) use (&$attempt) { - if ($attempt === 0 && $options['transaction'] instanceof Transaction) { - $transaction = $options['transaction']; - } elseif ($attempt === 0 && $options['transaction']) { - throw new \InvalidArgumentException('Given transaction must be an instance of Transaction.'); - } else { - $transaction = $this->operation->transaction($session, $options); - } + $transaction = $this->operation->transaction($session, $options); $attempt++; return $transaction; @@ -503,63 +673,26 @@ public function runTransaction(callable $operation, array $options = []) time_nanosleep($delay['seconds'], $delay['nanos']); }; - $commitFn = function ($operation, $session, $options) use ($startTransactionFn) { + $transactionFn = function ($operation, $session, $options) use ($startTransactionFn) { $transaction = call_user_func_array($startTransactionFn, [ $session, $options ]); - return call_user_func($operation, $transaction); - }; - - $retry = new Retry($options['maxRetries'], $delayFn); + $res = call_user_func($operation, $transaction); - try { - return $retry->execute($commitFn, [$operation, $session, $options]); - } finally { - $session->setExpiration(); - } - } + if ($transaction->state() === Transaction::STATE_ACTIVE) { + $transaction->rollback($options); + throw new \RuntimeException('Transactions must be rolled back or committed.'); + } - /** - * Create and return a new read/write Transaction. - * - * When manually using a Transaction, it is advised that retry logic be - * implemented to reapply all operations when an instance of - * {@see Google\Cloud\Core\Exception\AbortedException} is thrown. - * - * If you wish Google Cloud PHP to handle retry logic for you (recommended - * for most cases), use {@see Google\Cloud\Spanner\Database::runTransaction()}. - * - * Please note that once a transaction reads data, it will lock the read - * data, preventing other users from modifying that data. For this reason, - * it is important that every transaction commits or rolls back as early as - * possible. Do not hold transactions open longer than necessary. - * - * Example: - * ``` - * $transaction = $database->transaction(); - * ``` - * - * @codingStandardsIgnoreStart - * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest - * @see https://cloud.google.com/spanner/docs/transactions Transactions - * @codingStandardsIgnoreEnd - * - * @param array $options [optional] Configuration Options. - * @return Transaction - */ - public function transaction(array $options = []) - { - // There isn't anything configurable here. - $options['transactionOptions'] = [ - 'readWrite' => [] - ]; + return $res; + }; - $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $retry = new Retry($options['maxRetries'], $delayFn); try { - return $this->operation->transaction($session, $options); + return $retry->execute($transactionFn, [$operation, $session, $options]); } finally { $session->setExpiration(); } @@ -629,23 +762,15 @@ public function insertBatch($table, array $dataSet, array $options = []) $mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data); } - $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - - $options['singleUseTransaction'] = $this->configureTransactionOptions(); - - try { - return $this->operation->commit($session, $mutations, $options); - } finally { - $session->setExpiration(); - } + return $this->commitInSingleUseTransaction($mutations, $options); } /** * Update a row. * - * Only data which you wish to update need be included. You must provide - * enough information for the API to determine which row should be modified. - * In most cases, this means providing values for the Primary Key fields. + * Only data which you wish to update need be included. The list of columns + * must contain enough columns to allow Cloud Spanner to derive values for + * all primary key columns in the row to be modified. * * Mutations are committed in a single-use transaction. * @@ -674,9 +799,9 @@ public function update($table, array $data, array $options = []) /** * Update multiple rows. * - * Only data which you wish to update need be included. You must provide - * enough information for the API to determine which row should be modified. - * In most cases, this means providing values for the Primary Key fields. + * Only data which you wish to update need be included. The list of columns + * must contain enough columns to allow Cloud Spanner to derive values for + * all primary key columns in the row(s) to be modified. * * Mutations are committed in a single-use transaction. * @@ -709,15 +834,7 @@ public function updateBatch($table, array $dataSet, array $options = []) $mutations[] = $this->operation->mutation(Operation::OP_UPDATE, $table, $data); } - $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - - $options['singleUseTransaction'] = $this->configureTransactionOptions(); - - try { - return $this->operation->commit($session, $mutations, $options); - } finally { - $session->setExpiration(); - } + return $this->commitInSingleUseTransaction($mutations, $options); } /** @@ -792,23 +909,15 @@ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) $mutations[] = $this->operation->mutation(Operation::OP_INSERT_OR_UPDATE, $table, $data); } - $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - - $options['singleUseTransaction'] = $this->configureTransactionOptions(); - - try { - return $this->operation->commit($session, $mutations, $options); - } finally { - $session->setExpiration(); - } + return $this->commitInSingleUseTransaction($mutations, $options); } /** * Replace a row. * - * Provide data for the entire row. Google Cloud Spanner will attempt to - * find a record matching the Primary Key, and will replace the entire row. - * If a matching row is not found, it will be inserted. + * Provide data for the entire row. Cloud Spanner will attempt to find a + * record matching the Primary Key, and will replace the entire row. If a + * matching row is not found, it will be inserted. * * Mutations are committed in a single-use transaction. * @@ -838,9 +947,9 @@ public function replace($table, array $data, array $options = []) /** * Replace multiple rows. * - * Provide data for the entire row. Google Cloud Spanner will attempt to - * find a record matching the Primary Key, and will replace the entire row. - * If a matching row is not found, it will be inserted. + * Provide data for the entire row. Cloud Spanner will attempt to find a + * record matching the Primary Key, and will replace the entire row. If a + * matching row is not found, it will be inserted. * * Mutations are committed in a single-use transaction. * @@ -875,15 +984,7 @@ public function replaceBatch($table, array $dataSet, array $options = []) $mutations[] = $this->operation->mutation(Operation::OP_REPLACE, $table, $data); } - $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - - $options['singleUseTransaction'] = $this->configureTransactionOptions(); - - try { - return $this->operation->commit($session, $mutations, $options); - } finally { - $session->setExpiration(); - } + return $this->commitInSingleUseTransaction($mutations, $options); } /** @@ -915,15 +1016,7 @@ public function delete($table, KeySet $keySet, array $options = []) { $mutations = [$this->operation->deleteMutation($table, $keySet)]; - $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - - $options['singleUseTransaction'] = $this->configureTransactionOptions(); - - try { - return $this->operation->commit($session, $mutations, $options); - } finally { - $session->setExpiration(); - } + return $this->commitInSingleUseTransaction($mutations, $options); } /** @@ -948,7 +1041,8 @@ public function delete($table, KeySet $keySet, array $options = []) * 'parameters' => [ * 'postId' => 1337 * ], - * 'begin' => true + * 'begin' => true, + * 'transactionType' => SessionPoolInterface::CONTEXT_READ * ]); * * $result->rows()->current(); @@ -979,17 +1073,25 @@ public function delete($table, KeySet $keySet, array $options = []) * @param string $sql The query string to execute. * @param array $options [optional] { * Configuration Options. - * * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions) - * for detailed description of available transaction options. - * - * Please note that only one of `$strong`, `$minReadTimestamp`, + * for detailed description of available transaction options. Please + * note that only one of `$strong`, `$minReadTimestamp`, * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in * a request. * * @type array $parameters A key/value array of Query Parameters, where * the key is represented in the query string prefixed by a `@` * symbol. + * @type array $types A key/value array of Query Parameter types. + * Generally, Google Cloud PHP can infer types. Explicit type + * definitions are only necessary for null parameter values. + * Accepted values are defined as constants on + * {@see Google\Cloud\Spanner\ValueMapper}, and are as follows: + * `ValueMapper::TYPE_BOOL`, `ValueMapper::TYPE_INT64`, + * `ValueMapper::TYPE_FLOAT64`, `ValueMapper::TYPE_TIMESTAMP`, + * `ValueMapper::TYPE_DATE`, `ValueMapper::TYPE_STRING`, + * `ValueMapper::TYPE_BYTES`, `ValueMapper::TYPE_ARRAY` and + * `ValueMapper::TYPE_STRUCT`. * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected * read timestamp is included in the Transaction message that * describes the transaction. @@ -1011,8 +1113,8 @@ public function delete($table, KeySet $keySet, array $options = []) * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ` * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is * chosen, any snapshot options will be disregarded. If `$begin` - * is false, this option will be ignored. **Defaults to** - * `SessionPoolInterface::CONTEXT_READ`. + * is false, transaction type MUST be `SessionPoolInterface::CONTEXT_READ`. + * **Defaults to** `SessionPoolInterface::CONTEXT_READ`. * } * @codingStandardsIgnoreEnd * @return Result @@ -1059,7 +1161,8 @@ public function execute($sql, array $options = []) * $columns = ['ID', 'title', 'content']; * * $result = $database->read('Posts', $keySet, $columns, [ - * 'begin' => true + * 'begin' => true, + * 'transactionType' => SessionPoolInterface::CONTEXT_READ * ]); * * $result->rows()->current(); @@ -1102,7 +1205,6 @@ public function execute($sql, array $options = []) * a request. * * @type string $index The name of an index on the table. - * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected * read timestamp is included in the Transaction message that @@ -1125,8 +1227,8 @@ public function execute($sql, array $options = []) * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ` * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is * chosen, any snapshot options will be disregarded. If `$begin` - * is false, this option will be ignored. **Defaults to** - * `SessionPoolInterface::CONTEXT_READ`. + * is false, transaction type MUST be `SessionPoolInterface::CONTEXT_READ`. + * **Defaults to** `SessionPoolInterface::CONTEXT_READ`. * } * @codingStandardsIgnoreEnd * @return Result @@ -1211,11 +1313,7 @@ public function __destruct() public function createSession(array $options = []) { $res = $this->connection->createSession($options + [ - 'database' => GrpcSpannerClient::formatDatabaseName( - $this->projectId, - $this->instance->name(), - $this->name - ) + 'database' => $this->name ]); return $this->session($res['name']); @@ -1297,17 +1395,34 @@ private function selectSession($context = SessionPoolInterface::CONTEXT_READ) } } + private function commitInSingleUseTransaction(array $mutations, array $options = []) + { + $options['mutations'] = $mutations; + + return $this->runTransaction(function (Transaction $t) use ($options) { + return $t->commit($options); + }, [ + 'singleUse' => true + ]); + } + /** * Convert the simple database name to a fully qualified name. * * @return string */ - private function fullyQualifiedDatabaseName() + private function fullyQualifiedDatabaseName($name) { - return GrpcSpannerClient::formatDatabaseName( - $this->projectId, - $this->instance->name(), - $this->name - ); + $instance = InstanceAdminClient::parseInstanceFromInstanceName($this->instance->name()); + + try { + return GrpcSpannerClient::formatDatabaseName( + $this->projectId, + $instance, + $name + ); + } catch (ValidationException $e) { + return $name; + } } } diff --git a/src/Spanner/Date.php b/src/Spanner/Date.php index 8822c2bd9f7a..713ae879e06d 100644 --- a/src/Spanner/Date.php +++ b/src/Spanner/Date.php @@ -52,6 +52,27 @@ public function __construct(\DateTimeInterface $value) $this->value = $value; } + /** + * Create a Date from integer or string values. + * + * Example: + * ``` + * $date = Date::createFromValues(1995, 02, 04); + * ``` + * + * @param int|string $year The year. + * @param int|string $month The month (as a number). + * @param int|string $day The day of the month. + * @return Date + */ + public static function createFromValues($year, $month, $day) + { + $value = sprintf('%s-%s-%s', $year, $month, $day); + $dt = \DateTimeImmutable::createFromFormat(self::FORMAT, $value); + + return new static($dt); + } + /** * Get the underlying `\DateTimeInterface` implementation. * diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 30746a269879..2c9bfa5dfe03 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -22,18 +22,19 @@ use Google\Cloud\Core\Iam\Iam; use Google\Cloud\Core\Iterator\ItemIterator; use Google\Cloud\Core\Iterator\PageIterator; -use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\LongRunning\LROTrait; use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamInstance; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\GAX\ValidationException; use google\spanner\admin\instance\v1\Instance\State; /** - * Represents a Google Cloud Spanner instance + * Represents a Cloud Spanner instance * * Example: * ``` @@ -43,6 +44,41 @@ * * $instance = $spanner->instance('my-instance'); * ``` + * + * @method resumeOperation() { + * Resume a Long Running Operation + * + * Example: + * ``` + * $operation = $instance->resumeOperation($operationName); + * ``` + * + * @param string $operationName The Long Running Operation name. + * @param array $info [optional] The operation data. + * @return LongRunningOperation + * } + * @method longRunningOperations() { + * List long running operations. + * + * Example: + * ``` + * $operations = $instance->longRunningOperations(); + * ``` + * + * @param array $options [optional] { + * Configuration Options. + * + * @type string $name The name of the operation collection. + * @type string $filter The standard list filter. + * @type int $pageSize Maximum number of results to return per + * request. + * @type int $resultLimit Limit the number of results returned in total. + * **Defaults to** `0` (return all results). + * @type string $pageToken A previously-returned page token used to + * resume the loading of results from a specific point. + * } + * @return ItemIterator + * } */ class Instance { @@ -52,21 +88,13 @@ class Instance const STATE_READY = State::READY; const STATE_CREATING = State::CREATING; + const DEFAULT_NODE_COUNT = 1; + /** * @var ConnectionInterface */ private $connection; - /** - * @var LongRunningConnectionInterface - */ - private $lroConnection; - - /** - * @var array - */ - private $lroCallables; - /** * @var string */ @@ -93,15 +121,15 @@ class Instance private $iam; /** - * Create an object representing a Google Cloud Spanner instance. + * Create an object representing a Cloud Spanner instance. * * @param ConnectionInterface $connection The connection to the - * Google Cloud Spanner Admin API. + * Cloud Spanner Admin API. * @param LongRunningConnectionInterface $lroConnection An implementation * mapping to methods which handle LRO resolution in the service. * @param array $lroCallables * @param string $projectId The project ID. - * @param string $name The instance name. + * @param string $name The instance name or ID. * @param bool $returnInt64AsObject [optional] If true, 64 bit integers will be * returned as a {@see Google\Cloud\Core\Int64} object for 32 bit platform * compatibility. **Defaults to** false. @@ -117,12 +145,12 @@ public function __construct( array $info = [] ) { $this->connection = $connection; - $this->lroConnection = $lroConnection; - $this->lroCallables = $lroCallables; $this->projectId = $projectId; - $this->name = $name; + $this->name = $this->fullyQualifiedInstanceName($name, $projectId); $this->returnInt64AsObject = $returnInt64AsObject; $this->info = $info; + + $this->setLroProperties($lroConnection, $lroCallables, $this->name); } /** @@ -207,12 +235,58 @@ public function exists(array $options = []) public function reload(array $options = []) { $this->info = $this->connection->getInstance($options + [ - 'name' => $this->fullyQualifiedInstanceName() + 'name' => $this->name ]); return $this->info; } + /** + * Create a new instance. + * + * Example: + * ``` + * $operation = $instance->create($configuration); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#createinstancerequest CreateInstanceRequest + * + * @param InstanceConfiguration $config The configuration to use + * @param string $name The instance name + * @param array $options [optional] { + * Configuration options + * + * @type string $displayName **Defaults to** the value of $name. + * @type int $nodeCount **Defaults to** `1`. + * @type array $labels For more information, see + * [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). + * } + * @return LongRunningOperation + * @codingStandardsIgnoreEnd + */ + public function create(InstanceConfiguration $config, array $options = []) + { + $instanceId = InstanceAdminClient::parseInstanceFromInstanceName($this->name); + $options += [ + 'displayName' => $instanceId, + 'nodeCount' => self::DEFAULT_NODE_COUNT, + 'labels' => [], + ]; + + // This must always be set to CREATING, so overwrite anything else. + $options['state'] = State::CREATING; + + $operation = $this->connection->createInstance([ + 'instanceId' => $instanceId, + 'name' => $this->name, + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), + 'config' => $config->name() + ] + $options); + + return $this->resumeOperation($operation['name'], $operation); + } + /** * Return the instance state. * @@ -269,21 +343,11 @@ public function state(array $options = []) */ public function update(array $options = []) { - $info = $this->info($options); - - $options += [ - 'displayName' => $info['displayName'], - 'nodeCount' => (isset($info['nodeCount'])) ? $info['nodeCount'] : null, - 'labels' => (isset($info['labels'])) - ? $info['labels'] - : [] - ]; - $operation = $this->connection->updateInstance([ - 'name' => $this->fullyQualifiedInstanceName(), + 'name' => $this->name, ] + $options); - return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables); + return $this->resumeOperation($operation['name'], $operation); } /** @@ -304,7 +368,7 @@ public function update(array $options = []) public function delete(array $options = []) { return $this->connection->deleteInstance($options + [ - 'name' => $this->fullyQualifiedInstanceName() + 'name' => $this->name ]); } @@ -313,7 +377,7 @@ public function delete(array $options = []) * * Example: * ``` - * $database = $instance->createDatabase('my-database'); + * $operation = $instance->createDatabase('my-database'); * ``` * * @codingStandardsIgnoreStart @@ -325,24 +389,17 @@ public function delete(array $options = []) * Configuration Options * * @type array $statements Additional DDL statements. + * @type SessionPoolInterface $sessionPool A pool used to manage + * sessions. * } * @return LongRunningOperation */ public function createDatabase($name, array $options = []) { - $options += [ - 'statements' => [], - ]; + $instantiation = $this->pluckArray(['sessionPool'], $options); - $statement = sprintf('CREATE DATABASE `%s`', $name); - - $operation = $this->connection->createDatabase([ - 'instance' => $this->fullyQualifiedInstanceName(), - 'createStatement' => $statement, - 'extraStatements' => $options['statements'] - ]); - - return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables); + $database = $this->database($name, $instantiation); + return $database->create($options); } /** @@ -406,11 +463,10 @@ public function databases(array $options = []) return new ItemIterator( new PageIterator( function (array $database) { - $name = DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']); - return $this->database($name); + return $this->database($database['name']); }, [$this->connection, 'listDatabases'], - $options + ['instance' => $this->fullyQualifiedInstanceName()], + $options + ['instance' => $this->name], [ 'itemsKey' => 'databases', 'resultLimit' => $resultLimit @@ -434,7 +490,7 @@ public function iam() if (!$this->iam) { $this->iam = new Iam( new IamInstance($this->connection), - $this->fullyQualifiedInstanceName() + $this->name ); } @@ -444,11 +500,20 @@ public function iam() /** * Convert the simple instance name to a fully qualified name. * + * @param string $name The instance name. + * @param string $project The project ID. * @return string */ - private function fullyQualifiedInstanceName() + private function fullyQualifiedInstanceName($name, $project) { - return InstanceAdminClient::formatInstanceName($this->projectId, $this->name); + // try { + return InstanceAdminClient::formatInstanceName( + $project, + $name + ); + // } catch (ValidationException $e) { + // return $name; + // } } /** diff --git a/src/Spanner/Configuration.php b/src/Spanner/InstanceConfiguration.php similarity index 82% rename from src/Spanner/Configuration.php rename to src/Spanner/InstanceConfiguration.php index 1fbc1da9a4b1..ffa08fb62363 100644 --- a/src/Spanner/Configuration.php +++ b/src/Spanner/InstanceConfiguration.php @@ -20,9 +20,10 @@ use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; +use Google\GAX\ValidationException; /** - * Represents a Cloud Spanner Configuration. + * Represents a Cloud Spanner Instance Configuration. * * Example: * ``` @@ -30,14 +31,14 @@ * * $spanner = new SpannerClient(); * - * $configuration = $spanner->configuration('regional-europe-west'); + * $configuration = $spanner->instanceConfiguration('regional-europe-west'); * ``` * * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig InstanceConfig * @codingStandardsIgnoreEnd */ -class Configuration +class InstanceConfiguration { /** * @var ConnectionInterface @@ -60,12 +61,12 @@ class Configuration private $info; /** - * Create a configuration instance. + * Create an instance configuration object. * * @param ConnectionInterface $connection A service connection for the * Spanner API. * @param string $projectId The current project ID. - * @param string $name The simple configuration name. + * @param string $name The configuration name or ID. * @param array $info [optional] A service representation of the * configuration. */ @@ -77,7 +78,7 @@ public function __construct( ) { $this->connection = $connection; $this->projectId = $projectId; - $this->name = $name; + $this->name = $this->fullyQualifiedConfigName($name, $projectId); $this->info = $info; } @@ -167,8 +168,8 @@ public function exists(array $options = []) */ public function reload(array $options = []) { - $this->info = $this->connection->getConfig($options + [ - 'name' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $this->name), + $this->info = $this->connection->getInstanceConfig($options + [ + 'name' => $this->name, 'projectId' => $this->projectId ]); @@ -190,4 +191,23 @@ public function __debugInfo() 'info' => $this->info, ]; } + + /** + * Get the fully qualified instance config name. + * + * @param string $name The configuration name. + * @param string $projectId The project ID. + * @return string + */ + private function fullyQualifiedConfigName($name, $projectId) + { + try { + return InstanceAdminClient::formatInstanceConfigName( + $projectId, + $name + ); + } catch (ValidationException $e) { + return $name; + } + } } diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php index db5a432ba029..3e6b00324467 100644 --- a/src/Spanner/KeyRange.php +++ b/src/Spanner/KeyRange.php @@ -18,7 +18,7 @@ namespace Google\Cloud\Spanner; /** - * Represents a Google Cloud Spanner KeyRange. + * Represents a Cloud Spanner KeyRange. * * Example: * ``` @@ -110,6 +110,30 @@ public function __construct(array $options = []) $this->end = $options['end']; } + /** + * Returns a key range that covers all keys where the first components match. + * + * Equivalent to calling `KeyRange::__construct()` with closed type for start + * and end, and the same key for the start and end. + * + * Example: + * ``` + * $range = KeyRange::prefixMatch($key); + * ``` + * + * @param array $key The key to match against. + * @return KeyRange + */ + public static function prefixMatch(array $key) + { + return new static([ + 'startType' => self::TYPE_CLOSED, + 'endType' => self::TYPE_CLOSED, + 'start' => $key, + 'end' => $key + ]); + } + /** * Get the range start. * diff --git a/src/Spanner/KeySet.php b/src/Spanner/KeySet.php index 1ee4259f9bac..ea3893a814da 100644 --- a/src/Spanner/KeySet.php +++ b/src/Spanner/KeySet.php @@ -20,7 +20,7 @@ use Google\Cloud\Core\ValidateTrait; /** - * Represents a Google Cloud Spanner KeySet. + * Represents a Cloud Spanner KeySet. * * Example: * ``` @@ -74,6 +74,14 @@ public function __construct(array $options = []) $this->validateBatch($options['ranges'], KeyRange::class); + if (!is_array($options['keys'])) { + throw new \InvalidArgumentException('$options.keys must be an array.'); + } + + if (!is_bool($options['all'])) { + throw new \InvalidArgumentException('$options.all must be a boolean.'); + } + $this->keys = $options['keys']; $this->ranges = $options['ranges']; $this->all = (bool) $options['all']; @@ -230,10 +238,20 @@ public function keySetObject() $ranges[] = $range->keyRangeObject(); } - return [ - 'keys' => $this->keys, - 'ranges' => $ranges, - 'all' => $this->all - ]; + $set = []; + + if ($this->all) { + $set['all'] = true; + } + + if ($this->keys) { + $set['keys'] = $this->keys; + } + + if ($ranges) { + $set['ranges'] = $ranges; + } + + return $set; } } diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index e9bddbf9a88e..bc4fde898770 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -24,9 +24,9 @@ use Google\Cloud\Spanner\Session\SessionPoolInterface; /** - * Common interface for running operations against Google Cloud Spanner. This - * class is intended for internal use by the client library only. Implementors - * should access these operations via {@see Google\Cloud\Spanner\Database} or + * Common interface for running operations against Cloud Spanner. This class is + * intended for internal use by the client library only. Implementors should + * access these operations via {@see Google\Cloud\Spanner\Database} or * {@see Google\Cloud\Spanner\Transaction}. * * Usage examples may be found in classes making use of this class: @@ -163,11 +163,12 @@ public function execute(Session $session, $sql, array $options = []) { $options += [ 'parameters' => [], + 'types' => [], 'transactionContext' => null ]; $parameters = $this->pluck('parameters', $options); - $options += $this->mapper->formatParamsForExecuteSql($parameters); + $options += $this->mapper->formatParamsForExecuteSql($parameters, $options['types']); $context = $this->pluck('transactionContext', $options); @@ -234,12 +235,28 @@ public function read(Session $session, $table, KeySet $keySet, array $columns, a * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest * * @param Session $session The session to start the transaction in. - * @param array $options [optional] Configuration options. + * @param array $options [optional] { + * Configuration Options. + * + * @type bool $singleUse If true, a Transaction ID will not be allocated + * up front. Instead, the transaction will be considered + * "single-use", and may be used for only a single operation. + * **Defaults to** `false`. + * } * @return Transaction */ public function transaction(Session $session, array $options = []) { - $res = $this->beginTransaction($session, $options); + $options += [ + 'singleUse' => false + ]; + + if (!$options['singleUse']) { + $res = $this->beginTransaction($session, $options); + } else { + $res = []; + } + return $this->createTransaction($session, $res); } @@ -249,25 +266,44 @@ public function transaction(Session $session, array $options = []) * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest * * @param Session $session The session to start the snapshot in. - * @param array $options [optional] Configuration options. + * @param array $options [optional] { + * Configuration Options. + * + * @type bool $singleUse If true, a Transaction ID will not be allocated + * up front. Instead, the transaction will be considered + * "single-use", and may be used for only a single operation. + * **Defaults to** `false`. + * } * @return Snapshot */ public function snapshot(Session $session, array $options = []) { - $res = $this->beginTransaction($session, $options); + $options += [ + 'singleUse' => false + ]; - return $this->createSnapshot($session, $res); + if (!$options['singleUse']) { + $res = $this->beginTransaction($session, $options); + } else { + $res = []; + } + + return $this->createSnapshot($session, $res + $options); } /** * Create a Transaction instance from a response object. * * @param Session $session The session the transaction belongs to. - * @param array $res The transaction response. + * @param array $res [optional] The createTransaction response. * @return Transaction */ - public function createTransaction(Session $session, array $res) + public function createTransaction(Session $session, array $res = []) { + $res += [ + 'id' => null + ]; + return new Transaction($this, $session, $res['id']); } @@ -275,17 +311,21 @@ public function createTransaction(Session $session, array $res) * Create a Snapshot instance from a response object. * * @param Session $session The session the snapshot belongs to. - * @param array $res The snapshot response. + * @param array $res [optional] The createTransaction response. * @return Snapshot */ - public function createSnapshot(Session $session, array $res) + public function createSnapshot(Session $session, array $res = []) { - $timestamp = null; - if (isset($res['readTimestamp'])) { - $timestamp = $this->mapper->createTimestampWithNanos($res['readTimestamp']); + $res += [ + 'id' => null, + 'readTimestamp' => null + ]; + + if ($res['readTimestamp']) { + $res['readTimestamp'] = $this->mapper->createTimestampWithNanos($res['readTimestamp']); } - return new Snapshot($this, $session, $res['id'], $timestamp); + return new Snapshot($this, $session, $res); } /** diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index 40b1d09309f7..727f2e6fe3b8 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -20,9 +20,10 @@ use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\Timestamp; /** - * Represent a Google Cloud Spanner lookup result (either read or executeSql). + * Represent a Cloud Spanner lookup result (either read or executeSql). * * Example: * ``` diff --git a/src/Spanner/Session/CacheSessionPool.php b/src/Spanner/Session/CacheSessionPool.php index da881bc7860b..2d8702d2cb1e 100644 --- a/src/Spanner/Session/CacheSessionPool.php +++ b/src/Spanner/Session/CacheSessionPool.php @@ -49,7 +49,7 @@ * use Symfony\Component\Cache\Adapter\FilesystemAdapter; * * $spanner = new SpannerClient(); - * $cache = new FilesystemAdapter() + * $cache = new FilesystemAdapter(); * $sessionPool = new CacheSessionPool($cache); * * $database = $spanner->connect('my-instance', 'my-database', [ @@ -244,7 +244,8 @@ public function release(Session $session) unset($data['inUse'][$session->name()]); array_push($data['queue'], [ 'name' => $session->name(), - 'expiration' => $session->expiration() ?: $this->time() + SessionPoolInterface::SESSION_EXPIRATION_SECONDS + 'expiration' => $session->expiration() + ?: $this->time() + SessionPoolInterface::SESSION_EXPIRATION_SECONDS ]); $this->cacheItemPool->save($item->set($data)); }); diff --git a/src/Spanner/Snapshot.php b/src/Spanner/Snapshot.php index 8655e7d5349e..6a7ba5bd8ab8 100644 --- a/src/Spanner/Snapshot.php +++ b/src/Spanner/Snapshot.php @@ -105,9 +105,9 @@ * @return string * } */ -class Snapshot +class Snapshot implements TransactionalReadInterface { - use TransactionReadTrait; + use TransactionalReadTrait; /** * @var Timestamp @@ -117,20 +117,39 @@ class Snapshot /** * @param Operation $operation The Operation instance. * @param Session $session The session to use for spanner interactions. - * @param string $transactionId The Transaction ID. - * @param Timestamp $readTimestamp [optional] The read timestamp. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $id The Transaction ID. If no ID is provided, + * the Transaction will be a Single-Use Transaction. + * @type Timestamp $readTimestamp The read timestamp. + * } */ public function __construct( Operation $operation, Session $session, - $transactionId, - Timestamp $readTimestamp = null + array $options = [] ) { $this->operation = $operation; $this->session = $session; - $this->transactionId = $transactionId; - $this->readTimestamp = $readTimestamp; - $this->context = SessionPoolInterface::CONTEXT_READWRITE; + + $options += [ + 'id' => null, + 'readTimestamp' => null + ]; + + if ($options['readTimestamp'] && !($options['readTimestamp'] instanceof Timestamp)) { + throw new \InvalidArgumentException('$options.readTimestamp must be an instance of Timestamp.'); + } + + $this->transactionId = $options['id'] ?: null; + $this->readTimestamp = $options['readTimestamp']; + $this->type = $options['id'] + ? self::TYPE_PRE_ALLOCATED + : self::TYPE_SINGLE_USE; + + $this->context = SessionPoolInterface::CONTEXT_READ; + $this->options = $options; } /** diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index c28c4ef0db68..5b72de2a684a 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -34,9 +34,9 @@ use Psr\Http\StreamInterface; /** - * Google Cloud Spanner is a highly scalable, transactional, managed, NewSQL + * Cloud Spanner is a highly scalable, transactional, managed, NewSQL * database service. Find more information at - * [Google Cloud Spanner docs](https://cloud.google.com/spanner/). + * [Cloud Spanner docs](https://cloud.google.com/spanner/). * * Example: * ``` @@ -44,6 +44,19 @@ * * $spanner = new SpannerClient(); * ``` + * + * @method resumeOperation() { + * Resume a Long Running Operation + * + * Example: + * ``` + * $operation = $spanner->resumeOperation($operationName); + * ``` + * + * @param string $operationName The Long Running Operation name. + * @param array $info [optional] The operation data. + * @return LongRunningOperation + * } */ class SpannerClient { @@ -56,18 +69,12 @@ class SpannerClient const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data'; const ADMIN_SCOPE = 'https://www.googleapis.com/auth/spanner.admin'; - const DEFAULT_NODE_COUNT = 1; /** * @var ConnectionInterface */ protected $connection; - /** - * @var LongRunningConnectionInterface - */ - private $lroConnection; - /** * @var bool */ @@ -110,9 +117,9 @@ public function __construct(array $config = []) ]; $this->connection = new Grpc($this->configureAuthentication($config)); - $this->lroConnection = new LongRunningConnection($this->connection); $this->returnInt64AsObject = $config['returnInt64AsObject']; - $this->lroCallables = [ + + $this->setLroProperties(new LongRunningConnection($this->connection), [ [ 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', 'callable' => function ($instance) { @@ -135,15 +142,15 @@ public function __construct(array $config = []) return $this->instance($name, $instance); } ] - ]; + ]); } /** - * List all available configurations. + * List all available instance configurations. * * Example: * ``` - * $configurations = $spanner->configurations(); + * $configurations = $spanner->instanceConfigurations(); * ``` * * @codingStandardsIgnoreStart @@ -160,19 +167,18 @@ public function __construct(array $config = []) * @type string $pageToken A previously-returned page token used to * resume the loading of results from a specific point. * } - * @return ItemIterator + * @return ItemIterator */ - public function configurations(array $options = []) + public function instanceConfigurations(array $options = []) { $resultLimit = $this->pluck('resultLimit', $options, false) ?: 0; return new ItemIterator( new PageIterator( function (array $config) { - $name = InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config['name']); - return $this->configuration($name, $config); + return $this->instanceConfiguration($config['name'], $config); }, - [$this->connection, 'listConfigs'], + [$this->connection, 'listInstanceConfigs'], ['projectId' => InstanceAdminClient::formatProjectName($this->projectId)] + $options, [ 'itemsKey' => 'instanceConfigs', @@ -183,17 +189,17 @@ function (array $config) { } /** - * Get a configuration by its name. + * Get an instance configuration by its name. * * NOTE: This method does not execute a service request and does not verify * the existence of the given configuration. Unless you know with certainty * that the configuration exists, it is advised that you use - * {@see Google\Cloud\Spanner\Configuration::exists()} to verify existence - * before attempting to use the configuration. + * {@see Google\Cloud\Spanner\InstanceConfiguration::exists()} to verify + * existence before attempting to use the configuration. * * Example: * ``` - * $configuration = $spanner->configuration($configurationName); + * $configuration = $spanner->instanceConfiguration($configurationName); * ``` * * @codingStandardsIgnoreStart @@ -202,11 +208,11 @@ function (array $config) { * * @param string $name The Configuration name. * @param array $config [optional] The configuration details. - * @return Configuration + * @return InstanceConfiguration */ - public function configuration($name, array $config = []) + public function instanceConfiguration($name, array $config = []) { - return new Configuration($this->connection, $this->projectId, $name, $config); + return new InstanceConfiguration($this->connection, $this->projectId, $name, $config); } /** @@ -220,7 +226,7 @@ public function configuration($name, array $config = []) * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#createinstancerequest CreateInstanceRequest * - * @param Configuration $config The configuration to use + * @param InstanceConfiguration $config The configuration to use * @param string $name The instance name * @param array $options [optional] { * Configuration options @@ -233,26 +239,10 @@ public function configuration($name, array $config = []) * @return LongRunningOperation * @codingStandardsIgnoreEnd */ - public function createInstance(Configuration $config, $name, array $options = []) + public function createInstance(InstanceConfiguration $config, $name, array $options = []) { - $options += [ - 'displayName' => $name, - 'nodeCount' => self::DEFAULT_NODE_COUNT, - 'labels' => [], - 'operationName' => null, - ]; - - // This must always be set to CREATING, so overwrite anything else. - $options['state'] = State::CREATING; - - $operation = $this->connection->createInstance([ - 'instanceId' => $name, - 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), - 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), - 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) - ] + $options); - - return $this->lro($this->lroConnection, $operation['name'], $this->lroCallables); + $instance = $this->instance($name); + return $instance->create($config, $options); } /** @@ -504,20 +494,4 @@ public function duration($seconds, $nanos = 0) { return new Duration($seconds, $nanos); } - - /** - * Resume a Long Running Operation - * - * Example: - * ``` - * $operation = $spanner->resumeOperation($operationName); - * ``` - * - * @param string $operationName The Long Running Operation name. - * @return LongRunningOperation - */ - public function resumeOperation($operationName) - { - return $this->lro($this->lroConnection, $operationName, $this->lroCallables); - } } diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index e9c124324e38..86c4ae32e448 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -17,12 +17,13 @@ namespace Google\Cloud\Spanner; +use Google\Cloud\Core\Exception\AbortedException; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use RuntimeException; /** - * Manages interaction with Google Cloud Spanner inside a Transaction. + * Manages interaction with Cloud Spanner inside a Transaction. * * Transactions can be started via * {@see Google\Cloud\Spanner\Database::runTransaction()} (recommended) or via @@ -65,6 +66,8 @@ * @method execute() { * Run a query. * + * Note that this method is not available in a single-use transaction. + * * Example: * ``` * $result = $transaction->execute( @@ -94,6 +97,8 @@ * @method read() { * Lookup rows in a table. * + * Note that this method is not available in a single-use transaction. + * * Example: * ``` * $keySet = new KeySet([ @@ -132,37 +137,34 @@ * @return string * } */ -class Transaction +class Transaction implements TransactionalReadInterface { - use TransactionReadTrait; - - const STATE_ACTIVE = 0; - const STATE_ROLLED_BACK = 1; - const STATE_COMMITTED = 2; + use TransactionalReadTrait; /** * @var array */ private $mutations = []; - /** - * @var int - */ - private $state = self::STATE_ACTIVE; - /** * @param Operation $operation The Operation instance. * @param Session $session The session to use for spanner interactions. - * @param string $transactionId The Transaction ID. + * @param string $transactionId [optional] The Transaction ID. If no ID is + * provided, the Transaction will be a Single-Use Transaction. */ public function __construct( Operation $operation, Session $session, - $transactionId + $transactionId = null ) { $this->operation = $operation; $this->session = $session; $this->transactionId = $transactionId; + + $this->type = $transactionId + ? self::TYPE_PRE_ALLOCATED + : self::TYPE_SINGLE_USE; + $this->context = SessionPoolInterface::CONTEXT_READWRITE; } @@ -395,7 +397,11 @@ public function delete($table, KeySet $keySet) public function rollback(array $options = []) { if ($this->state !== self::STATE_ACTIVE) { - throw new \RuntimeException('The transaction cannot be rolled back because it is not active'); + throw new \BadMethodCallException('The transaction cannot be rolled back because it is not active'); + } + + if ($this->type === self::TYPE_SINGLE_USE) { + throw new \BadMethodCallException('Cannot roll back a single-use transaction.'); } $this->state = self::STATE_ROLLED_BACK; @@ -416,21 +422,39 @@ public function rollback(array $options = []) * $transaction->commit(); * ``` * - * @param array $options [optional] Configuration Options. + * @param array $options [optional] { + * Configuration Options. + * + * @type array $mutations An array of mutations to commit. May be used + * instead of or in addition to enqueing mutations separately. + * } * @return Timestamp The commit timestamp. - * @throws \RuntimeException If the transaction is not active - * @throws \AbortedException If the commit is aborted for any reason. + * @throws \BadMethodCall If the transaction is not active or already used. + * @throws AbortedException If the commit is aborted for any reason. */ public function commit(array $options = []) { if ($this->state !== self::STATE_ACTIVE) { - throw new \RuntimeException('The transaction cannot be committed because it is not active'); + throw new \BadMethodCallException('The transaction cannot be committed because it is not active'); + } + + if (!$this->singleUseState()) { + $this->state = self::STATE_COMMITTED; } - $this->state = self::STATE_COMMITTED; + $options += [ + 'mutations' => [] + ]; + + $options['mutations'] += $this->mutations; $options['transactionId'] = $this->transactionId; - return $this->operation->commit($this->session, $this->mutations, $options); + + $t = $this->transactionOptions($options); + + $options[$t[1]] = $t[0]; + + return $this->operation->commit($this->session, $this->pluck('mutations', $options), $options); } /** @@ -451,11 +475,6 @@ public function state() return $this->state; } - public function id() - { - return $this->transactionId; - } - /** * Format, validate and enqueue mutations in the transaction. * diff --git a/src/Spanner/TransactionConfigurationTrait.php b/src/Spanner/TransactionConfigurationTrait.php index a5f5e61b4c61..ac7d9582f3b5 100644 --- a/src/Spanner/TransactionConfigurationTrait.php +++ b/src/Spanner/TransactionConfigurationTrait.php @@ -21,7 +21,7 @@ use Google\Cloud\Spanner\Session\SessionPoolInterface; /** - * Manage shared transaction configuration. + * Configure transaction selection for read, executeSql, rollback and commit. */ trait TransactionConfigurationTrait { @@ -34,25 +34,60 @@ trait TransactionConfigurationTrait * or read/write. * * @param array $options call options. - * @return array + * @param array $previous Previously given call options (for single-use snapshots). + * @return array [(array) transaction selector, (string) context] */ - private function transactionSelector(array &$options) + private function transactionSelector(array &$options, array $previous = []) { $options += [ 'begin' => false, 'transactionType' => SessionPoolInterface::CONTEXT_READ, - 'transactionId' => null + ]; + + $res = $this->transactionOptions($options, $previous); + + // TransactionSelector uses a different key name for singleUseTransaction + // and transactionId than transactionOptions, so we'll rewrite those here + // so transactionOptions works as expected for commitRequest. + + $type = $res[1]; + if ($type === 'singleUseTransaction') { + $type = 'singleUse'; + } elseif ($type === 'transactionId') { + $type = 'id'; + } + + return [ + [$type => $res[0]], + $res[2] + ]; + } + + /** + * Return transaction options based on given configuration options. + * + * @param array $options call options. + * @param array $previous Previously given call options (for single-use snapshots). + * @return array [(array) transaction options, (string) transaction type, (string) context] + */ + private function transactionOptions(array &$options, array $previous = []) + { + $options += [ + 'begin' => false, + 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE, + 'transactionId' => null, ]; $type = null; $context = $this->pluck('transactionType', $options); $id = $this->pluck('transactionId', $options); + if (!is_null($id)) { - $type = 'id'; + $type = 'transactionId'; $transactionOptions = $id; } elseif ($context === SessionPoolInterface::CONTEXT_READ) { - $transactionOptions = $this->configureSnapshotOptions($options); + $transactionOptions = $this->configureSnapshotOptions($options, $previous); } elseif ($context === SessionPoolInterface::CONTEXT_READWRITE) { $transactionOptions = $this->configureTransactionOptions(); } else { @@ -64,13 +99,10 @@ private function transactionSelector(array &$options) $begin = $this->pluck('begin', $options); if (is_null($type)) { - $type = ($begin) ? 'begin' : 'singleUse'; + $type = ($begin) ? 'begin' : 'singleUseTransaction'; } - return [ - [$type => $transactionOptions], - $context - ]; + return [$transactionOptions, $type, $context]; } private function configureTransactionOptions() @@ -81,14 +113,16 @@ private function configureTransactionOptions() } /** - * Create a Read Only single use transaction. + * Configure a Read-Only transaction. * * @param array $options Configuration Options. + * @param array $previous Previously given call options (for single-use snapshots). * @return array */ - private function configureSnapshotOptions(array &$options) + private function configureSnapshotOptions(array &$options, array $previous = []) { $options += [ + 'singleUse' => false, 'returnReadTimestamp' => null, 'strong' => null, 'readTimestamp' => null, @@ -97,6 +131,17 @@ private function configureSnapshotOptions(array &$options) 'maxStaleness' => null, ]; + $previousOptions = isset($previous['transactionOptions']['readOnly']) + ? $previous['transactionOptions']['readOnly'] + : []; + + // These are only available in single-use transactions. + if (!$options['singleUse'] && ($options['maxStaleness'] || $options['minReadTimestamp'])) { + throw new \BadMethodCallException( + 'maxStaleness and minReadTimestamp are only available in single-use transactions.' + ); + } + $transactionOptions = [ 'readOnly' => $this->arrayFilterRemoveNull([ 'returnReadTimestamp' => $this->pluck('returnReadTimestamp', $options), @@ -105,7 +150,7 @@ private function configureSnapshotOptions(array &$options) 'maxStaleness' => $this->pluck('maxStaleness', $options), 'readTimestamp' => $this->pluck('readTimestamp', $options), 'exactStaleness' => $this->pluck('exactStaleness', $options), - ]) + ]) + $previousOptions ]; if (empty($transactionOptions['readOnly'])) { @@ -123,7 +168,7 @@ private function configureSnapshotOptions(array &$options) ]; foreach ($timestampFields as $tsf) { - if (isset($transactionOptions['readOnly'][$tsf])) { + if (isset($transactionOptions['readOnly'][$tsf]) && !isset($previousOptions[$tsf])) { $field = $transactionOptions['readOnly'][$tsf]; if (!($field instanceof Timestamp)) { throw new \BadMethodCallException(sprintf( @@ -137,7 +182,7 @@ private function configureSnapshotOptions(array &$options) } foreach ($durationFields as $df) { - if (isset($transactionOptions['readOnly'][$df])) { + if (isset($transactionOptions['readOnly'][$df]) && !isset($previousOptions[$df])) { $field = $transactionOptions['readOnly'][$df]; if (!($field instanceof Duration)) { throw new \BadMethodCallException(sprintf( diff --git a/src/Spanner/TransactionalReadInterface.php b/src/Spanner/TransactionalReadInterface.php new file mode 100644 index 000000000000..0b6ae3d11e76 --- /dev/null +++ b/src/Spanner/TransactionalReadInterface.php @@ -0,0 +1,67 @@ +context; + $this->singleUseState(); + $this->checkReadContext(); + $options['transactionId'] = $this->transactionId; + $options['transactionType'] = $this->context; - list($transactionOptions, $context) = $this->transactionSelector($options); - $options['transaction'] = $transactionOptions; - $options['transactionContext'] = $context; + $selector = $this->transactionSelector($options, $this->options); + + $options['transaction'] = $selector[0]; return $this->operation->execute($this->session, $sql, $options); } @@ -79,19 +99,21 @@ public function execute($sql, array $options = []) * Configuration Options. * * @type string $index The name of an index on the table. - * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. * } * @return Result */ public function read($table, KeySet $keySet, array $columns, array $options = []) { - $options['transactionType'] = $this->context; + $this->singleUseState(); + $this->checkReadContext(); + $options['transactionId'] = $this->transactionId; + $options['transactionType'] = $this->context; + $options += $this->options; + $selector = $this->transactionSelector($options, $this->options); - list($transactionOptions, $context) = $this->transactionSelector($options); - $options['transaction'] = $transactionOptions; - $options['transactionContext'] = $context; + $options['transaction'] = $selector[0]; return $this->operation->read($this->session, $table, $keySet, $columns, $options); } @@ -99,10 +121,54 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] /** * Retrieve the Transaction ID. * - * @return string + * @return string|null */ public function id() { return $this->transactionId; } + + /** + * Get the Transaction Type. + * + * @return int + */ + public function type() + { + return $this->type; + } + + /** + * Check the transaction state, and update as necessary for single-use transactions. + * + * @return bool true if transaction is single use, false otherwise. + * @throws \BadMethodCallException + */ + private function singleUseState() + { + if ($this->type === self::TYPE_SINGLE_USE) { + if ($this->state === self::STATE_SINGLE_USE_USED) { + throw new \BadMethodCallException('This single-use transaction has already been used.'); + } + + $this->state = self::STATE_SINGLE_USE_USED; + + return true; + } + + return false; + } + + /** + * Check whether the context is valid for a read operation. Reads are not + * allowed in single-use read-write transactions. + * + * @throws \BadMethodCallException + */ + private function checkReadContext() + { + if ($this->type === self::TYPE_SINGLE_USE && $this->context === SessionPoolInterface::CONTEXT_READWRITE) { + throw new \BadMethodCallException('Cannot use a single-use read-write transaction for read or execute.'); + } + } } diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index a88419ad1441..8bd3507e90d4 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -28,6 +28,7 @@ namespace Google\Cloud\Spanner\V1; +use DrSlump\Protobuf\Codec\PhpArray; use Google\GAX\AgentHeaderDescriptor; use Google\GAX\ApiCallable; use Google\GAX\CallSettings; diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index 39c6620e07d0..8118fe72df65 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -39,7 +39,21 @@ class ValueMapper const TYPE_BYTES = TypeCode::TYPE_BYTES; const TYPE_ARRAY = TypeCode::TYPE_ARRAY; const TYPE_STRUCT = TypeCode::TYPE_STRUCT; - const TYPE_NULL = 'null'; + + /** + * @var array + */ + private $allowedTypes = [ + self::TYPE_BOOL, + self::TYPE_INT64, + self::TYPE_FLOAT64, + self::TYPE_TIMESTAMP, + self::TYPE_DATE, + self::TYPE_STRING, + self::TYPE_BYTES, + self::TYPE_ARRAY, + self::TYPE_STRUCT, + ]; /** * @var bool @@ -60,14 +74,32 @@ public function __construct($returnInt64AsObject) * an array of parameters and inferred parameter types. * * @param array $parameters The key/value parameters. + * @param array $types The types of values. * @return array An associative array containing params and paramTypes. */ - public function formatParamsForExecuteSql(array $parameters) + public function formatParamsForExecuteSql(array $parameters, array $types = []) { $paramTypes = []; foreach ($parameters as $key => $value) { - list ($parameters[$key], $paramTypes[$key]) = $this->paramType($value); + if (is_null($value) && !isset($types[$key])) { + throw new \BadMethodCallException(sprintf( + 'Null value for parameter @%s must supply a parameter type.', + $key + )); + } + + $type = isset($types[$key]) ? $types[$key] : null; + + if ($type !== null && !in_array($type, $this->allowedTypes)) { + throw new \BadMethodCallException(sprintf( + 'Type %s given for parameter @%s is not valid.', + $type, + $key + )); + } + + list ($parameters[$key], $paramTypes[$key]) = $this->paramType($value, $type); } return [ @@ -135,6 +167,7 @@ public function createTimestampWithNanos($timestamp) $timestamp = preg_replace(self::NANO_REGEX, '.000000Z', $timestamp); $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $timestamp); + return new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0); } @@ -223,7 +256,7 @@ private function decodeValue($value, array $type) * @param mixed $value The PHP value * @return array The Value type */ - private function paramType($value) + private function paramType($value, $givenType = null) { $phpType = gettype($value); switch ($phpType) { @@ -255,7 +288,7 @@ private function paramType($value) case 'array': if ($this->isAssoc($value)) { - throw new \InvalidArgumentException( + throw new \BadMethodCallException( 'Associative arrays are not supported. Did you mean to call a batch method?' ); } @@ -265,11 +298,13 @@ private function paramType($value) foreach ($value as $element) { $type = $this->paramType($element); $res[] = $type[0]; - $types[] = $type[1]['code']; + if (isset($type[1]['code'])) { + $types[] = $type[1]['code']; + } } if (count(array_unique($types)) !== 1) { - throw new \InvalidArgumentException('Array values may not be of mixed type'); + throw new \BadMethodCallException('Array values may not be of mixed type'); } $type = $this->typeObject( @@ -282,7 +317,7 @@ private function paramType($value) break; case 'NULL': - $type = null; + $type = $this->typeObject($givenType); break; default: diff --git a/tests/snippets/Language/LanguageClientTest.php b/tests/snippets/Language/LanguageClientTest.php index f2462709c9b2..d9dd2f9f89ef 100644 --- a/tests/snippets/Language/LanguageClientTest.php +++ b/tests/snippets/Language/LanguageClientTest.php @@ -33,7 +33,7 @@ class LanguageClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = \Google\Cloud\Dev\stub(NaturalLanguageClient::class); + $this->client = \Google\Cloud\Dev\stub(LanguageClient::class); $this->client->___setProperty('connection', $this->connection->reveal()); } diff --git a/tests/snippets/PubSub/PubSubClientTest.php b/tests/snippets/PubSub/PubSubClientTest.php index 4f1e2ab4b904..5c2fcff18d45 100644 --- a/tests/snippets/PubSub/PubSubClientTest.php +++ b/tests/snippets/PubSub/PubSubClientTest.php @@ -196,7 +196,7 @@ public function testCreateSnapshot() ->shouldBecalled() ->willReturn([]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(PubSubClient::class, 'createSnapshot'); $snippet->addLocal('pubsub', $this->client); @@ -228,7 +228,7 @@ public function testSnapshots() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('snapshots'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); diff --git a/tests/snippets/PubSub/SnapshotTest.php b/tests/snippets/PubSub/SnapshotTest.php index 27469cfa9e8d..9f4c144d7da2 100644 --- a/tests/snippets/PubSub/SnapshotTest.php +++ b/tests/snippets/PubSub/SnapshotTest.php @@ -38,7 +38,7 @@ class SnapshotTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->snapshot = new \SnapshotStub( + $this->snapshot = \Google\Cloud\Dev\stub(Snapshot::class, [ $this->connection->reveal(), self::PROJECT, self::SNAPSHOT, @@ -47,7 +47,7 @@ public function setUp() 'topic' => 'foo', 'subscription' => 'bar' ] - ); + ]); } public function testClass() @@ -89,7 +89,7 @@ public function testCreate() ->shouldBeCalled() ->willReturn($info); - $this->snapshot->setConnection($this->connection->reveal()); + $this->snapshot->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(Snapshot::class, 'create'); $snippet->addLocal('snapshot', $this->snapshot); @@ -103,7 +103,7 @@ public function testDelete() $this->connection->deleteSnapshot(Argument::any()) ->shouldBeCalled(); - $this->snapshot->setConnection($this->connection->reveal()); + $this->snapshot->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(Snapshot::class, 'delete'); $snippet->addLocal('snapshot', $this->snapshot); diff --git a/tests/snippets/PubSub/SubscriptionTest.php b/tests/snippets/PubSub/SubscriptionTest.php index ddfce689118e..df9c6e9f94de 100644 --- a/tests/snippets/PubSub/SubscriptionTest.php +++ b/tests/snippets/PubSub/SubscriptionTest.php @@ -103,7 +103,7 @@ public function testUpdate() $this->connection->updateSubscription(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -299,7 +299,7 @@ public function testSeekToTime() $this->connection->seek(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -312,7 +312,7 @@ public function testSeekToSnapshot() $this->connection->seek(Argument::any()) ->shouldBeCalled(); - $this->subscription->setConnection($this->connection->reveal()); + $this->subscription->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } diff --git a/tests/snippets/ServiceBuilderTest.php b/tests/snippets/ServiceBuilderTest.php index 85e352bf0e18..ab9bc1e6c886 100644 --- a/tests/snippets/ServiceBuilderTest.php +++ b/tests/snippets/ServiceBuilderTest.php @@ -57,7 +57,7 @@ public function serviceBuilderMethods() ['logging', LoggingClient::class, 'logging'], ['language', LanguageClient::class, 'language'], ['pubsub', PubSubClient::class, 'pubsub'], - ['spanner', SpannerClient::class, 'spanner'], + ['spanner', SpannerClient::class, 'spanner', true], ['speech', SpeechClient::class, 'speech'], ['storage', StorageClient::class, 'storage'], ['trace', TraceClient::class, 'trace'], @@ -69,8 +69,14 @@ public function serviceBuilderMethods() /** * @dataProvider serviceBuilderMethods */ - public function testServices($method, $returnType, $returnName) + public function testServices($method, $returnType, $returnName, $skipIfMissingGrpc = false) { + if ($skipIfMissingGrpc) { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + } + $snippet = $this->snippetFromMethod(ServiceBuilder::class, $method); $snippet->addLocal('cloud', $this->cloud); $res = $snippet->invoke($returnName); diff --git a/tests/snippets/Spanner/BytesTest.php b/tests/snippets/Spanner/BytesTest.php index 5b1a629247a6..00209c6f3e8c 100644 --- a/tests/snippets/Spanner/BytesTest.php +++ b/tests/snippets/Spanner/BytesTest.php @@ -38,6 +38,10 @@ public function setUp() public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(Bytes::class); $res = $snippet->invoke('bytes'); $this->assertInstanceOf(Bytes::class, $res->returnVal()); diff --git a/tests/snippets/Spanner/DatabaseTest.php b/tests/snippets/Spanner/DatabaseTest.php index 36d85e57b480..bbc7910928a9 100644 --- a/tests/snippets/Spanner/DatabaseTest.php +++ b/tests/snippets/Spanner/DatabaseTest.php @@ -18,8 +18,12 @@ namespace Google\Cloud\Tests\Snippets\Spanner; use Google\Cloud\Core\Iam\Iam; +use Google\Cloud\Core\Iterator\ItemIterator; use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Dev\Snippet\SnippetTestCase; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; @@ -50,7 +54,7 @@ class DatabaseTest extends SnippetTestCase public function setUp() { $instance = $this->prophesize(Instance::class); - $instance->name()->willReturn(self::INSTANCE); + $instance->name()->willReturn(InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE)); $session = $this->prophesize(Session::class); @@ -69,7 +73,7 @@ public function setUp() self::PROJECT, self::DATABASE, $sessionPool->reveal() - ], ['connection', 'operation']); + ], ['connection', 'operation', 'lroConnection']); } private function stubOperation() @@ -83,18 +87,26 @@ private function stubOperation() public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(Database::class); $res = $snippet->invoke('database'); $this->assertInstanceOf(Database::class, $res->returnVal()); - $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + $this->assertEquals(self::DATABASE, DatabaseAdminClient::parseDatabaseFromDatabaseName($res->returnVal()->name())); } public function testClassViaInstance() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(Database::class, 1); $res = $snippet->invoke('database'); $this->assertInstanceOf(Database::class, $res->returnVal()); - $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + $this->assertEquals(self::DATABASE, DatabaseAdminClient::parseDatabaseFromDatabaseName($res->returnVal()->name())); } public function testName() @@ -102,7 +114,7 @@ public function testName() $snippet = $this->snippetFromMethod(Database::class, 'name'); $snippet->addLocal('database', $this->database); $res = $snippet->invoke('name'); - $this->assertEquals(self::DATABASE, $res->returnVal()); + $this->assertEquals(self::DATABASE, DatabaseAdminClient::parseDatabaseFromDatabaseName($res->returnVal())); } /** @@ -113,7 +125,7 @@ public function testExists() $snippet = $this->snippetFromMethod(Database::class, 'exists'); $snippet->addLocal('database', $this->database); - $this->connection->getDatabaseDDL(Argument::any()) + $this->connection->getDatabase(Argument::any()) ->shouldBeCalled() ->willReturn(['statements' => []]); @@ -123,6 +135,68 @@ public function testExists() $this->assertEquals('Database exists!', $res->output()); } + /** + * @group spanneradmin + */ + public function testInfo() + { + $db = ['name' => 'foo']; + + $snippet = $this->snippetFromMethod(Database::class, 'info'); + $snippet->addLocal('database', $this->database); + + $this->connection->getDatabase(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($db); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('info'); + $this->assertEquals($db, $res->returnVal()); + $snippet->invoke(); + } + + /** + * @group spanneradmin + */ + public function testReload() + { + $db = ['name' => 'foo']; + + $snippet = $this->snippetFromMethod(Database::class, 'reload'); + $snippet->addLocal('database', $this->database); + + $this->connection->getDatabase(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn($db); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('info'); + $this->assertEquals($db, $res->returnVal()); + $snippet->invoke(); + } + + /** + * @group spanneradmin + */ + public function testCreate() + { + $snippet = $this->snippetFromMethod(Database::class, 'create'); + $snippet->addLocal('database', $this->database); + + $this->connection->createDatabase(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'my-operation' + ]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('operation'); + $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); + } + /** * @group spanneradmin */ @@ -131,8 +205,11 @@ public function testUpdateDdl() $snippet = $this->snippetFromMethod(Database::class, 'updateDdl'); $snippet->addLocal('database', $this->database); - $this->connection->updateDatabase(Argument::any()) - ->shouldBeCalled(); + $this->connection->updateDatabaseDdl(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'my-operation' + ]); $this->database->___setProperty('connection', $this->connection->reveal()); @@ -147,8 +224,11 @@ public function testUpdateDdlBatch() $snippet = $this->snippetFromMethod(Database::class, 'updateDdlBatch'); $snippet->addLocal('database', $this->database); - $this->connection->updateDatabase(Argument::any()) - ->shouldBeCalled(); + $this->connection->updateDatabaseDdl(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'my-operation' + ]); $this->database->___setProperty('connection', $this->connection->reveal()); @@ -539,6 +619,7 @@ public function testExecuteBeginSnapshot() $snippet = $this->snippetFromMethod(Database::class, 'execute', 1); $snippet->addLocal('database', $this->database); + $snippet->addUse(SessionPoolInterface::class); $res = $snippet->invoke('result'); $this->assertInstanceOf(Result::class, $res->returnVal()); @@ -637,6 +718,7 @@ public function testReadWithSnapshot() $snippet = $this->snippetFromMethod(Database::class, 'read', 1); $snippet->addLocal('database', $this->database); $snippet->addUse(KeySet::class); + $snippet->addUse(SessionPoolInterface::class); $res = $snippet->invoke('result'); $this->assertInstanceOf(Result::class, $res->returnVal()); @@ -704,6 +786,40 @@ public function testIam() $this->assertInstanceOf(Iam::class, $res->returnVal()); } + public function testResumeOperation() + { + $snippet = $this->snippetFromMagicMethod(Database::class, 'resumeOperation'); + $snippet->addLocal('database', $this->database); + $snippet->addLocal('operationName', 'foo'); + + $res = $snippet->invoke('operation'); + $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); + $this->assertEquals('foo', $res->returnVal()->name()); + } + + public function testLongRunningOperations() + { + $snippet = $this->snippetFromMethod(Database::class, 'longRunningOperations'); + $snippet->addLocal('database', $this->database); + + $lroConnection = $this->prophesize(LongRunningConnectionInterface::class); + $lroConnection->operations(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'operations' => [ + [ + 'name' => 'foo' + ] + ] + ]); + + $this->database->___setProperty('lroConnection', $lroConnection->reveal()); + + $res = $snippet->invoke('operations'); + $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); + $this->assertContainsOnlyInstancesOf(LongRunningOperation::class, $res->returnVal()); + } + private function resultGenerator(array $data) { yield $data; diff --git a/tests/snippets/Spanner/DateTest.php b/tests/snippets/Spanner/DateTest.php index 96998c73fa27..0ac50dc03239 100644 --- a/tests/snippets/Spanner/DateTest.php +++ b/tests/snippets/Spanner/DateTest.php @@ -37,6 +37,10 @@ public function setUp() public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(Date::class); $res = $snippet->invoke('date'); @@ -52,6 +56,15 @@ public function testClassString() $this->assertEquals($this->date->formatAsString(), $res->output()); } + public function testCreateFromValues() + { + $snippet = $this->snippetFromMethod(Date::class, 'createFromValues'); + $snippet->addUse(Date::class); + + $res = $snippet->invoke('date'); + $this->assertEquals('1995-02-04', $res->returnVal()->formatAsString()); + } + public function testGet() { $snippet = $this->snippetFromMethod(Date::class, 'get'); diff --git a/tests/snippets/Spanner/DurationTest.php b/tests/snippets/Spanner/DurationTest.php index 228c65463153..35747a9fc90e 100644 --- a/tests/snippets/Spanner/DurationTest.php +++ b/tests/snippets/Spanner/DurationTest.php @@ -37,6 +37,10 @@ public function setUp() public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(Duration::class); $res = $snippet->invoke('duration'); $this->assertInstanceOf(Duration::class, $res->returnVal()); diff --git a/tests/snippets/Spanner/ConfigurationTest.php b/tests/snippets/Spanner/InstanceConfigurationTest.php similarity index 62% rename from tests/snippets/Spanner/ConfigurationTest.php rename to tests/snippets/Spanner/InstanceConfigurationTest.php index 181b5fecebdf..ca5a59f03f2f 100644 --- a/tests/snippets/Spanner/ConfigurationTest.php +++ b/tests/snippets/Spanner/InstanceConfigurationTest.php @@ -18,15 +18,16 @@ namespace Google\Cloud\Tests\Snippets\Spanner; use Google\Cloud\Dev\Snippet\SnippetTestCase; -use Google\Cloud\Spanner\Configuration; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; +use Google\Cloud\Spanner\InstanceConfiguration; use Prophecy\Argument; /** * @group spanner * @group spanneradmin */ -class ConfigurationTest extends SnippetTestCase +class InstanceConfigurationTest extends SnippetTestCase { const PROJECT = 'my-awesome-project'; const CONFIG = 'regional-europe-west'; @@ -37,7 +38,7 @@ class ConfigurationTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->config = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->config = \Google\Cloud\Dev\stub(InstanceConfiguration::class, [ $this->connection->reveal(), self::PROJECT, self::CONFIG @@ -46,33 +47,37 @@ public function setUp() public function testClass() { - $snippet = $this->snippetFromClass(Configuration::class); + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + + $snippet = $this->snippetFromClass(InstanceConfiguration::class); $res = $snippet->invoke('configuration'); - $this->assertInstanceOf(Configuration::class, $res->returnVal()); - $this->assertEquals(self::CONFIG, $res->returnVal()->name()); + $this->assertInstanceOf(InstanceConfiguration::class, $res->returnVal()); + $this->assertEquals(InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG), $res->returnVal()->name()); } public function testName() { - $snippet = $this->snippetFromMethod(Configuration::class, 'name'); + $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'name'); $snippet->addLocal('configuration', $this->config); $res = $snippet->invoke('name'); - $this->assertEquals(self::CONFIG, $res->returnVal()); + $this->assertEquals(InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG), $res->returnVal()); } public function testInfo() { - $snippet = $this->snippetFromMethod(Configuration::class, 'info'); + $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'info'); $snippet->addLocal('configuration', $this->config); $info = [ - 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG), 'displayName' => self::CONFIG ]; - $this->connection->getConfig(Argument::any()) + $this->connection->getInstanceConfig(Argument::any()) ->shouldBeCalled() ->willReturn($info); @@ -84,13 +89,13 @@ public function testInfo() public function testExists() { - $snippet = $this->snippetFromMethod(Configuration::class, 'exists'); + $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'exists'); $snippet->addLocal('configuration', $this->config); - $this->connection->getConfig(Argument::any()) + $this->connection->getInstanceConfig(Argument::any()) ->shouldBeCalled() ->willReturn([ - 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG), 'displayName' => self::CONFIG ]); @@ -103,14 +108,14 @@ public function testExists() public function testReload() { $info = [ - 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG), 'displayName' => self::CONFIG ]; - $snippet = $this->snippetFromMethod(Configuration::class, 'reload'); + $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'reload'); $snippet->addLocal('configuration', $this->config); - $this->connection->getConfig(Argument::any()) + $this->connection->getInstanceConfig(Argument::any()) ->shouldBeCalled() ->willReturn($info); diff --git a/tests/snippets/Spanner/InstanceTest.php b/tests/snippets/Spanner/InstanceTest.php index e30f2975c992..d9470db79108 100644 --- a/tests/snippets/Spanner/InstanceTest.php +++ b/tests/snippets/Spanner/InstanceTest.php @@ -22,9 +22,12 @@ use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Dev\Snippet\SnippetTestCase; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\InstanceConfiguration; use Prophecy\Argument; /** @@ -49,15 +52,41 @@ public function setUp() [], self::PROJECT, self::INSTANCE - ]); + ], ['connection', 'lroConnection']); } public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(Instance::class); $res = $snippet->invoke('instance'); $this->assertInstanceOf(Instance::class, $res->returnVal()); - $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + $this->assertEquals(InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE), $res->returnVal()->name()); + } + + /** + * @group spanneradmin + */ + public function testCreate() + { + $config = $this->prophesize(InstanceConfiguration::class); + $config->name()->willReturn(InstanceAdminClient::formatInstanceConfigName(self::PROJECT, 'foo')); + + $snippet = $this->snippetFromMethod(Instance::class, 'create'); + $snippet->addLocal('configuration', $config->reveal()); + $snippet->addLocal('instance', $this->instance); + + $this->connection->createInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn(['name' => 'operations/foo']); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('operation'); + $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); } public function testName() @@ -66,7 +95,7 @@ public function testName() $snippet->addLocal('instance', $this->instance); $res = $snippet->invoke('name'); - $this->assertEquals(self::INSTANCE, $res->returnVal()); + $this->assertEquals(InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE), $res->returnVal()); } public function testInfo() @@ -136,16 +165,12 @@ public function testUpdate() $snippet = $this->snippetFromMethod(Instance::class, 'update'); $snippet->addLocal('instance', $this->instance); - $this->connection->getInstance(Argument::any()) - ->shouldBeCalledTimes(1) + $this->connection->updateInstance(Argument::any()) + ->shouldBeCalled() ->willReturn([ - 'displayName' => 'foo', - 'nodeCount' => 1 + 'name' => 'my-operation' ]); - $this->connection->updateInstance(Argument::any()) - ->shouldBeCalled(); - $this->instance->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -168,11 +193,14 @@ public function testCreateDatabase() $snippet->addLocal('instance', $this->instance); $this->connection->createDatabase(Argument::any()) - ->shouldBeCalled(); + ->shouldBeCalled() + ->willReturn([ + 'name' => 'my-operation' + ]); $this->instance->___setProperty('connection', $this->connection->reveal()); - $res = $snippet->invoke('database'); + $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); } @@ -183,7 +211,7 @@ public function testDatabase() $res = $snippet->invoke('database'); $this->assertInstanceOf(Database::class, $res->returnVal()); - $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + $this->assertEquals(self::DATABASE, DatabaseAdminClient::parseDatabaseFromDatabaseName($res->returnVal()->name())); } public function testDatabases() @@ -196,7 +224,7 @@ public function testDatabases() ->willReturn([ 'databases' => [ [ - 'name' => 'projects/'. self::PROJECT .'/instances/'. self::INSTANCE .'/databases/'. self::DATABASE + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE) ] ] ]); @@ -217,4 +245,38 @@ public function testIam() $res = $snippet->invoke('iam'); $this->assertInstanceOf(Iam::class, $res->returnVal()); } + + public function testResumeOperation() + { + $snippet = $this->snippetFromMagicMethod(Instance::class, 'resumeOperation'); + $snippet->addLocal('instance', $this->instance); + $snippet->addLocal('operationName', 'foo'); + + $res = $snippet->invoke('operation'); + $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); + $this->assertEquals('foo', $res->returnVal()->name()); + } + + public function testLongRunningOperations() + { + $snippet = $this->snippetFromMethod(Instance::class, 'longRunningOperations'); + $snippet->addLocal('instance', $this->instance); + + $lroConnection = $this->prophesize(LongRunningConnectionInterface::class); + $lroConnection->operations(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'operations' => [ + [ + 'name' => 'foo' + ] + ] + ]); + + $this->instance->___setProperty('lroConnection', $lroConnection->reveal()); + + $res = $snippet->invoke('operations'); + $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); + $this->assertContainsOnlyInstancesOf(LongRunningOperation::class, $res->returnVal()); + } } diff --git a/tests/snippets/Spanner/KeyRangeTest.php b/tests/snippets/Spanner/KeyRangeTest.php index 869d3694a6bb..2efbb6f36c2b 100644 --- a/tests/snippets/Spanner/KeyRangeTest.php +++ b/tests/snippets/Spanner/KeyRangeTest.php @@ -34,12 +34,35 @@ public function setUp() public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(KeyRange::class); $snippet->addUse(KeyRange::class); $res = $snippet->invoke('range'); $this->assertInstanceOf(KeyRange::class, $res->returnVal()); } + public function testPrefixMatch() + { + $key = ['foo']; + + $range = new KeyRange([ + 'start' => $key, + 'end' => $key, + 'startType' => KeyRange::TYPE_CLOSED, + 'endType' => KeyRange::TYPE_CLOSED, + ]); + + $snippet = $this->snippetFromMethod(KeyRange::class, 'prefixMatch'); + $snippet->addLocal('key', $key); + $snippet->addUse(KeyRange::class); + $res = $snippet->invoke('range'); + + $this->assertEquals($range, $res->returnVal()); + } + public function testStart() { $this->range->setStart(KeyRange::TYPE_OPEN, ['Bob']); diff --git a/tests/snippets/Spanner/KeySetTest.php b/tests/snippets/Spanner/KeySetTest.php index c7a2904c53f6..af16d08ce719 100644 --- a/tests/snippets/Spanner/KeySetTest.php +++ b/tests/snippets/Spanner/KeySetTest.php @@ -37,6 +37,10 @@ public function setUp() public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(KeySet::class); $res = $snippet->invoke('keySet'); $this->assertInstanceOf(KeySet::class, $res->returnVal()); diff --git a/tests/snippets/Spanner/ResultTest.php b/tests/snippets/Spanner/ResultTest.php index 9315b1e56a25..766525d0c176 100644 --- a/tests/snippets/Spanner/ResultTest.php +++ b/tests/snippets/Spanner/ResultTest.php @@ -48,6 +48,8 @@ public function setUp() ->willReturn($this->prophesize(Snapshot::class)->reveal()); $result->transaction() ->willReturn($this->prophesize(Transaction::class)->reveal()); + $result->stats() + ->willReturn([]); $this->result = $result->reveal(); $database->execute(Argument::any()) ->willReturn($this->result); @@ -56,6 +58,10 @@ public function setUp() public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(Result::class); $snippet->replace('$database =', '//$database ='); $snippet->addLocal('database', $this->database); @@ -81,12 +87,22 @@ public function testMetadata() public function testStats() { - $snippet = $this->snippetFromMethod(Result::class, 'metadata'); + $snippet = $this->snippetFromMethod(Result::class, 'stats'); $snippet->addLocal('result', $this->result); - $res = $snippet->invoke('metadata'); + $res = $snippet->invoke('stats'); $this->assertInternalType('array', $res->returnVal()); } + public function testQueryWithStats() + { + $db = $this->prophesize(Database::class); + $db->execute(Argument::any(), ['queryMode' => 'PROFILE']); + + $snippet = $this->snippetFromMethod(Result::class, 'stats', 1); + $snippet->addLocal('database', $db->reveal()); + $snippet->invoke(); + } + public function testSnapshot() { $snippet = $this->snippetFromMethod(Result::class, 'snapshot'); diff --git a/tests/snippets/Spanner/Session/CacheSessionPoolTest.php b/tests/snippets/Spanner/Session/CacheSessionPoolTest.php new file mode 100644 index 000000000000..1849221e57af --- /dev/null +++ b/tests/snippets/Spanner/Session/CacheSessionPoolTest.php @@ -0,0 +1,42 @@ +markTestSkipped('Must have the grpc extension installed to run this test.'); + } + + $snippet = $this->snippetFromClass(CacheSessionPool::class); + $snippet->replace('$cache =', '//$cache ='); + $snippet->addLocal('cache', new MemoryCacheItemPool); + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/SnapshotTest.php b/tests/snippets/Spanner/SnapshotTest.php index 11b24f6e9cbd..52779a8aeac8 100644 --- a/tests/snippets/Spanner/SnapshotTest.php +++ b/tests/snippets/Spanner/SnapshotTest.php @@ -49,8 +49,10 @@ public function setUp() $this->snapshot = \Google\Cloud\Dev\stub(Snapshot::class, [ $operation->reveal(), $session->reveal(), - self::TRANSACTION, - new Timestamp(new \DateTime) + [ + 'id' => self::TRANSACTION, + 'readTimestamp' => new Timestamp(new \DateTime) + ] ], ['operation']); } @@ -69,6 +71,10 @@ private function stubOperation($stub = null) public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $database = $this->prophesize(Database::class); $database->snapshot()->shouldBeCalled()->willReturn('foo'); diff --git a/tests/snippets/Spanner/SpannerClientTest.php b/tests/snippets/Spanner/SpannerClientTest.php index 125a6c9f57af..2fa48f7dc296 100644 --- a/tests/snippets/Spanner/SpannerClientTest.php +++ b/tests/snippets/Spanner/SpannerClientTest.php @@ -21,13 +21,14 @@ use Google\Cloud\Core\Iterator\ItemIterator; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Dev\Snippet\SnippetTestCase; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Bytes; -use Google\Cloud\Spanner\Configuration; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\InstanceConfiguration; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\SpannerClient; @@ -39,7 +40,8 @@ */ class SpannerClientTest extends SnippetTestCase { - const CONFIG = 'Foo'; + const PROJECT = 'my-awesome-project'; + const CONFIG = 'foo'; const INSTANCE = 'my-instance'; private $client; @@ -47,6 +49,10 @@ class SpannerClientTest extends SnippetTestCase public function setUp() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $this->connection = $this->prophesize(ConnectionInterface::class); $this->client = \Google\Cloud\Dev\stub(SpannerClient::class); $this->client->___setProperty('connection', $this->connection->reveal()); @@ -62,43 +68,43 @@ public function testClass() /** * @group spanneradmin */ - public function testConfigurations() + public function testInstanceConfigurations() { - $this->connection->listConfigs(Argument::any()) + $this->connection->listInstanceConfigs(Argument::any()) ->shouldBeCalled() ->willReturn([ 'instanceConfigs' => [ - ['name' => 'projects/my-awesome-projects/instanceConfigs/Foo'], - ['name' => 'projects/my-awesome-projects/instanceConfigs/Bar'], + ['name' => 'projects/my-awesome-projects/instanceConfigs/foo'], + ['name' => 'projects/my-awesome-projects/instanceConfigs/bar'], ] ]); $this->client->___setProperty('connection', $this->connection->reveal()); - $snippet = $this->snippetFromMethod(SpannerClient::class, 'configurations'); + $snippet = $this->snippetFromMethod(SpannerClient::class, 'instanceConfigurations'); $snippet->addLocal('spanner', $this->client); $res = $snippet->invoke('configurations'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); - $this->assertInstanceOf(Configuration::class, $res->returnVal()->current()); - $this->assertEquals('Foo', $res->returnVal()->current()->name()); + $this->assertInstanceOf(InstanceConfiguration::class, $res->returnVal()->current()); + $this->assertEquals('projects/my-awesome-projects/instanceConfigs/foo', $res->returnVal()->current()->name()); } /** * @group spanneradmin */ - public function testConfiguration() + public function testInstanceConfiguration() { $configName = 'foo'; - $snippet = $this->snippetFromMethod(SpannerClient::class, 'configuration'); + $snippet = $this->snippetFromMethod(SpannerClient::class, 'instanceConfiguration'); $snippet->addLocal('spanner', $this->client); $snippet->addLocal('configurationName', self::CONFIG); $res = $snippet->invoke('configuration'); - $this->assertInstanceOf(Configuration::class, $res->returnVal()); - $this->assertEquals(self::CONFIG, $res->returnVal()->name()); + $this->assertInstanceOf(InstanceConfiguration::class, $res->returnVal()); + $this->assertEquals(InstanceAdminClient::formatInstanceConfigName(self::PROJECT, $configName), $res->returnVal()->name()); } /** @@ -108,7 +114,7 @@ public function testCreateInstance() { $snippet = $this->snippetFromMethod(SpannerClient::class, 'createInstance'); $snippet->addLocal('spanner', $this->client); - $snippet->addLocal('configuration', $this->client->configuration(self::CONFIG)); + $snippet->addLocal('configuration', $this->client->instanceConfiguration(self::CONFIG)); $this->connection->createInstance(Argument::any()) ->shouldBeCalled() @@ -130,7 +136,7 @@ public function testInstance() $res = $snippet->invoke('instance'); $this->assertInstanceOf(Instance::class, $res->returnVal()); - $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + $this->assertEquals(InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE), $res->returnVal()->name()); } /** @@ -145,8 +151,8 @@ public function testInstances() ->shouldBeCalled() ->willReturn([ 'instances' => [ - ['name' => 'projects/my-awesome-project/instances/'. self::INSTANCE], - ['name' => 'projects/my-awesome-project/instances/Bar'] + ['name' => InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE)], + ['name' => InstanceAdminClient::formatInstanceName(self::PROJECT, 'bar')] ] ]); @@ -155,7 +161,7 @@ public function testInstances() $res = $snippet->invoke('instances'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); $this->assertInstanceOf(Instance::class, $res->returnVal()->current()); - $this->assertEquals(self::INSTANCE, $res->returnVal()->current()->name()); + $this->assertEquals(InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE), $res->returnVal()->current()->name()); } public function testConnect() @@ -253,7 +259,7 @@ public function testDuration() public function testResumeOperation() { - $snippet = $this->snippetFromMethod(SpannerClient::class, 'resumeOperation'); + $snippet = $this->snippetFromMagicMethod(SpannerClient::class, 'resumeOperation'); $snippet->addLocal('spanner', $this->client); $snippet->addLocal('operationName', 'operations/foo'); diff --git a/tests/snippets/Spanner/TimestampTest.php b/tests/snippets/Spanner/TimestampTest.php index a96512fe0d11..1d51dcba2d53 100644 --- a/tests/snippets/Spanner/TimestampTest.php +++ b/tests/snippets/Spanner/TimestampTest.php @@ -36,6 +36,10 @@ public function setUp() public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $snippet = $this->snippetFromClass(Timestamp::class); $res = $snippet->invoke('timestamp'); $this->assertInstanceOf(Timestamp::class, $res->returnVal()); diff --git a/tests/snippets/Spanner/TransactionTest.php b/tests/snippets/Spanner/TransactionTest.php index 881adc9f9ca1..2c6596ef9e08 100644 --- a/tests/snippets/Spanner/TransactionTest.php +++ b/tests/snippets/Spanner/TransactionTest.php @@ -67,6 +67,10 @@ private function stubOperation($stub = null) public function testClass() { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + $database = $this->prophesize(Database::class); $database->runTransaction(Argument::type('callable'))->shouldBeCalled(); diff --git a/tests/snippets/bootstrap.php b/tests/snippets/bootstrap.php index 298292891bbb..3cabe2e11455 100644 --- a/tests/snippets/bootstrap.php +++ b/tests/snippets/bootstrap.php @@ -30,6 +30,8 @@ if (!empty($uncovered)) { echo sprintf("\033[31mNOTICE: %s uncovered snippets! \033[0m See build/snippets-uncovered.json for a report.\n", count($uncovered)); - exit(1); + if (extension_loaded('grpc')) { + exit(1); + } } }); diff --git a/tests/system/Spanner/AdminTest.php b/tests/system/Spanner/AdminTest.php index 545deb89989a..39ed97792f44 100644 --- a/tests/system/Spanner/AdminTest.php +++ b/tests/system/Spanner/AdminTest.php @@ -22,6 +22,7 @@ use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\InstanceConfiguration; /** * @group spanner @@ -34,15 +35,15 @@ public function testInstance() $instances = $client->instances(); $instance = array_filter(iterator_to_array($instances), function ($instance) { - return $instance->name() === self::INSTANCE_NAME; + return $this->parseName($instance->name()) === self::INSTANCE_NAME; }); - $this->assertInstanceOf(Instance::class, $instance[0]); + $this->assertInstanceOf(Instance::class, current($instance)); $instance = self::$instance; $this->assertTrue($instance->exists()); - $this->assertEquals($instance->name(), $this->parseName($instance->info()['name'])); - $this->assertEquals($instance->name(), $this->parseName($instance->reload()['name'])); + $this->assertEquals($instance->name(), $instance->info()['name']); + $this->assertEquals($instance->name(), $instance->reload()['name']); $this->assertEquals(Instance::STATE_READY, $instance->state()); @@ -73,7 +74,7 @@ public function testDatabase() $databases = $instance->databases(); $database = array_filter(iterator_to_array($databases), function ($db) use ($dbName) { - return $db->name() === $dbName; + return $this->parseDbName($db->name()) === $dbName; }); $this->assertInstanceOf(Database::class, current($database)); @@ -90,6 +91,27 @@ public function testDatabase() $this->assertEquals($db->ddl()[0], $stmt); } + public function testConfigurations() + { + $client = self::$client; + + $configurations = $client->instanceConfigurations(); + + $this->assertContainsOnly(InstanceConfiguration::class, $configurations); + + $res = iterator_to_array($configurations); + $firstConfigName = $res[0]->name(); + + $config = $client->instanceConfiguration($firstConfigName); + + $this->assertInstanceOf(InstanceConfiguration::class, $config); + $this->assertEquals($firstConfigName, $config->name()); + + $this->assertTrue($config->exists()); + $this->assertEquals($config->name(), $config->info()['name']); + $this->assertEquals($config->name(), $config->reload()['name']); + } + private function parseName($name) { return InstanceAdminClient::parseInstanceFromInstanceName($name); diff --git a/tests/system/Spanner/ConfigurationTest.php b/tests/system/Spanner/ConfigurationTest.php deleted file mode 100644 index 94ded41522f1..000000000000 --- a/tests/system/Spanner/ConfigurationTest.php +++ /dev/null @@ -1,53 +0,0 @@ -configurations(); - - $this->assertContainsOnly(Configuration::class, $configurations); - - $res = iterator_to_array($configurations); - $firstConfigName = $res[0]->name(); - - $config = $client->configuration($firstConfigName); - - $this->assertInstanceOf(Configuration::class, $config); - $this->assertEquals($firstConfigName, $config->name()); - - $this->assertTrue($config->exists()); - $this->assertEquals($config->name(), $this->parseName($config->info()['name'])); - $this->assertEquals($config->name(), $this->parseName($config->reload()['name'])); - } - - private function parseName($name) - { - return InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($name); - } -} diff --git a/tests/system/Spanner/OperationsTest.php b/tests/system/Spanner/OperationsTest.php index f5113845eec5..9214fdedbd1d 100644 --- a/tests/system/Spanner/OperationsTest.php +++ b/tests/system/Spanner/OperationsTest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Tests\System\Spanner; +use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\Timestamp; @@ -25,51 +26,62 @@ */ class OperationsTest extends SpannerTestCase { - private $id = 12345; + private static $id1; + private static $id2; + private static $name1; + private static $name2; + + public static function setUpBeforeClass() + { + self::$id1 = rand(1000,9999); + self::$id2 = rand(1,999); + self::$name1 = uniqid(self::TESTING_PREFIX); + self::$name2 = uniqid(self::TESTING_PREFIX); + + parent::setUpBeforeClass(); + + self::$database->insert(self::TEST_TABLE_NAME, [ + 'id' => self::$id1, + 'name' => self::$name1, + 'birthday' => new Date(new \DateTime('2000-01-01')) + ]); + } public function testInsert() { $db = self::$database; + $res = $db->insert(self::TEST_TABLE_NAME, [ - 'id' => $this->id, - 'name' => 'Bob', - 'birthday' => self::$client->date(new \DateTime('2000-01-01')) + 'id' => self::$id2, + 'name' => self::$name2, + 'birthday' => new Date(new \DateTime('2000-01-01')) ]); $this->assertInstanceOf(Timestamp::class, $res); } - /** - * @depends testInsert - */ public function testExecute() { $db = self::$database; $row = $this->getRow(); - $this->assertEquals($this->id, $row['id']); + $this->assertEquals(self::$id2, $row['id']); } - /** - * @depends testInsert - */ public function testRead() { $db = self::$database; $keySet = self::$client->keySet([ - 'keys' => [$this->id] + 'keys' => [self::$id2] ]); $columns = ['id', 'name']; $res = $db->read(self::TEST_TABLE_NAME, $keySet, $columns); $row = $res->rows()->current(); - $this->assertEquals($this->id, $row['id']); + $this->assertEquals(self::$id2, $row['id']); } - /** - * @depends testInsert - */ public function testUpdate() { $db = self::$database; @@ -82,14 +94,11 @@ public function testUpdate() $this->assertEquals('Doug', $row['name']); } - /** - * @depends testInsert - */ public function testInsertOrUpdate() { $db = self::$database; $db->insertOrUpdate('Users', [ - 'id' => $this->id, + 'id' => self::$id2, 'name' => 'Dave', 'birthday' => new Date(new \DateTime('1990-01-01')) ]); @@ -98,14 +107,11 @@ public function testInsertOrUpdate() $this->assertEquals('Dave', $row['name']); } - /** - * @depends testInsert - */ public function testReplace() { $db = self::$database; $db->replace('Users', [ - 'id' => $this->id, + 'id' => self::$id2, 'name' => 'John', 'birthday' => new Date(new \DateTime('1990-01-01')) ]); @@ -114,26 +120,84 @@ public function testReplace() $this->assertEquals('John', $row['name']); } - /** - * @depends testInsert - */ public function testDelete() { $db = self::$database; $keySet = self::$client->keySet([ - 'keys' => [$this->id] + 'keys' => [self::$id2] ]); $db->delete(self::TEST_TABLE_NAME, $keySet); $this->assertNull($this->getRow()); } + public function testEmptyRead() + { + $db = self::$database; + + $keySet = self::$client->keySet(['keys' => [99999]]); + + $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id','name']); + $this->assertEmpty(iterator_to_array($res->rows())); + } + + public function testEmptyReadOnIndex() + { + $db = self::$database; + + $keySet = self::$client->keySet(['keys' => [99999]]); + + $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id','name'], [ + 'index' => self::TEST_INDEX_NAME + ]); + + $this->assertEmpty(iterator_to_array($res->rows())); + } + + public function testReadSingleKeyFromIndex() + { + $db = self::$database; + + $keySet = self::$client->keySet(['keys' => [self::$name1]]); + + $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['name'], [ + 'index' => self::TEST_INDEX_NAME + ]); + + $this->assertEquals(self::$name1, $res->rows()->current()['name']); + } + + public function testReadNonExistentSingleKey() + { + $db = self::$database; + + $keySet = self::$client->keySet([ + 'keys' => [99999] + ]); + + $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id','name']); + $this->assertEmpty(iterator_to_array($res->rows())); + } + + public function testReadNonExistentSingleKeyFromIndex() + { + $db = self::$database; + + $keySet = self::$client->keySet(['keys' => ['foobar']]); + + $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['name'], [ + 'index' => self::TEST_INDEX_NAME + ]); + + $this->assertEmpty(iterator_to_array($res->rows())); + } + private function getRow() { $db = self::$database; $res = $db->execute('SELECT * FROM '. self::TEST_TABLE_NAME .' WHERE id=@id', [ 'parameters' => [ - 'id' => $this->id + 'id' => self::$id2 ] ]); diff --git a/tests/system/Spanner/SpannerTestCase.php b/tests/system/Spanner/SpannerTestCase.php index 264a5542a6dd..61c6f406182d 100644 --- a/tests/system/Spanner/SpannerTestCase.php +++ b/tests/system/Spanner/SpannerTestCase.php @@ -27,7 +27,9 @@ class SpannerTestCase extends \PHPUnit_Framework_TestCase { const TESTING_PREFIX = 'gcloud_testing_'; const INSTANCE_NAME = 'google-cloud-php-system-tests'; + const TEST_TABLE_NAME = 'Users'; + const TEST_INDEX_NAME = 'uniqueIndex'; protected static $client; protected static $instance; @@ -57,14 +59,18 @@ public static function setUpBeforeClass() self::$deletionQueue[] = function() use ($db) { $db->drop(); }; - $op = $db->updateDdl( + $db->updateDdl( 'CREATE TABLE '. self::TEST_TABLE_NAME .' ( id INT64 NOT NULL, name STRING(MAX) NOT NULL, birthday DATE NOT NULL ) PRIMARY KEY (id)' - ); - $op->pollUntilComplete(); + )->pollUntilComplete(); + + $db->updateDdl( + 'CREATE UNIQUE INDEX '. self::TEST_INDEX_NAME .' + ON '. self::TEST_TABLE_NAME .' (name)' + )->pollUntilComplete(); self::$database = $db; } diff --git a/tests/system/Spanner/TransactionTest.php b/tests/system/Spanner/TransactionTest.php index 1ce4f5825a81..66f3ff1613f0 100644 --- a/tests/system/Spanner/TransactionTest.php +++ b/tests/system/Spanner/TransactionTest.php @@ -18,12 +18,29 @@ namespace Google\Cloud\Tests\System\Spanner; use Google\Cloud\Spanner\Date; +use Google\Cloud\Spanner\KeySet; +use Google\Cloud\Spanner\Timestamp; /** - * @group spanner + * @group spannerz */ class TransactionTest extends SpannerTestCase { + private static $row = []; + + public static function setUpBeforeClass() + { + parent::setUpBeforeClass(); + + self::$row = [ + 'id' => rand(1000,9999), + 'name' => uniqid(self::TESTING_PREFIX), + 'birthday' => new Date(new \DateTime('2000-01-01')) + ]; + echo 'inserting row'.PHP_EOL; + self::$database->insert(self::TEST_TABLE_NAME, self::$row); + } + public function testRunTransaction() { $db = self::$database; @@ -43,4 +60,72 @@ public function testRunTransaction() $t->rollback(); }); } + + public function testStrongRead() + { + $db = self::$database; + + $snapshot = $db->snapshot([ + 'strong' => true, + 'returnReadTimestamp' => true + ]); + + list($keySet, $cols) = $this->readArgs(); + $res = $snapshot->read(self::TEST_TABLE_NAME, $keySet, $cols); + + $row = $res->rows()->current(); + + $this->assertEquals(self::$row, $row); + $this->assertInstanceOf(Timestamp::class, $snapshot->readTimestamp()); + } + + public function testExactTimestampRead() + { + $db = self::$database; + + $ts = new Timestamp(new \DateTimeImmutable); + + $row = $db->execute('SELECT * FROM '. self::TEST_TABLE_NAME .' WHERE id = @id', [ + 'parameters' => ['id' => self::$row['id']] + ])->rows()->current(); + $row['name'] = uniqid(self::TESTING_PREFIX); + + $db->update(self::TEST_TABLE_NAME, $row); + sleep(10); + + $snapshot = $db->snapshot([ + 'returnReadTimestamp' => true, + 'readTimestamp' => $ts + ]); + + list($keySet, $cols) = $this->readArgs(); + + echo "Cached row data (should match snapshot result)".PHP_EOL; + print_r(self::$row); + + $res = $snapshot->read(self::TEST_TABLE_NAME, $keySet, $cols)->rows(); + echo PHP_EOL."Snapshot Result". PHP_EOL; + print_r(iterator_to_array($res)); + + echo PHP_EOL."Database Result". PHP_EOL; + print_r(iterator_to_array($db->read(self::TEST_TABLE_NAME, $keySet, $cols)->rows())); + exit; + $row = $res->current(); + + $this->assertEquals($ts->get(), $snapshot->readTimestamp()->get()); + $this->assertEquals($row, self::$row); + + // Reset to previous state. + $db->update(self::TEST_TABLE_NAME, self::$row); + } + + private function readArgs() + { + return [ + new KeySet([ + 'keys' => [self::$row['id']] + ]), + array_keys(self::$row) + ]; + } } diff --git a/tests/unit/Spanner/BytesTest.php b/tests/unit/Spanner/BytesTest.php index d224738f96e2..d91b19ccfd34 100644 --- a/tests/unit/Spanner/BytesTest.php +++ b/tests/unit/Spanner/BytesTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Bytes; diff --git a/tests/unit/Spanner/Connection/GrpcTest.php b/tests/unit/Spanner/Connection/GrpcTest.php new file mode 100644 index 000000000000..9b486e493efb --- /dev/null +++ b/tests/unit/Spanner/Connection/GrpcTest.php @@ -0,0 +1,359 @@ +markTestSkipped('Must have the grpc extension installed to run this test.'); + } + + $this->requestWrapper = $this->prophesize(GrpcRequestWrapper::class); + $this->successMessage = 'success'; + } + + /** + * @dataProvider methodProvider + */ + public function testCallBasicMethods($method, $args, $expectedArgs, $return = null, $result = '') + { + $this->requestWrapper->send( + Argument::type('callable'), + $expectedArgs, + Argument::type('array') + )->willReturn($return ?: $this->successMessage); + + $grpc = new Grpc(); + $grpc->setRequestWrapper($this->requestWrapper->reveal()); + + $this->assertEquals($result !== '' ? $result : $this->successMessage, $grpc->$method($args)); + } + + public function methodProvider() + { + $codec = new PhpArray; + + $configName = 'test-config'; + $instanceName = 'test-instance'; + $policy = ['foo' => 'bar']; + $permissions = ['permission1','permission2']; + $databaseName = 'test-database'; + $sessionName = 'test-session'; + $transactionName = 'test-transaction'; + + $instanceArgs = [ + 'name' => $instanceName, + 'config' => $configName, + 'displayName' => $instanceName, + 'nodeCount' => 1, + 'state' => \google\spanner\admin\instance\v1\Instance\State::CREATING, + 'labels' => [] + ]; + + $instance = (new \google\spanner\admin\instance\v1\Instance) + ->deserialize(array_filter([ + 'labels' => $this->formatLabelsForApi([]) + ] + $instanceArgs), $codec); + + $lro = $this->prophesize(OperationResponse::class)->reveal(); + + $mask = array_keys($instance->serialize(new PhpArray([], false))); + $fieldMask = (new \google\protobuf\FieldMask())->deserialize(['paths' => $mask], $codec); + + $tableName = 'foo'; + + $createStmt = 'CREATE TABLE '. $tableName; + $sql = 'SELECT * FROM '. $tableName; + + $transactionSelector = (new TransactionSelector) + ->deserialize(['id' => $transactionName], $codec); + + $mapper = new ValueMapper(false); + $mapped = $mapper->formatParamsForExecuteSql(['foo' => 'bar']); + + $expectedParams = (new \google\protobuf\Struct) + ->deserialize($this->formatStructForApi($mapped['params']), $codec); + + $expectedParamTypes = $mapped['paramTypes']; + foreach ($expectedParamTypes as $key => $param) { + $expectedParamTypes[$key] = (new Type) + ->deserialize($param, $codec); + } + + $columns = ['id', 'name']; + $keySetArgs = []; + $keySet = (new KeySet) + ->deserialize($keySetArgs, $codec); + + $readWriteTransactionArgs = ['readWrite' => []]; + $readWriteTransactionOptions = new TransactionOptions; + $rw = new TransactionOptions\ReadWrite; + $readWriteTransactionOptions->setReadWrite($rw); + + $ts = (new \DateTime)->format('Y-m-d\TH:i:s.u\Z'); + $readOnlyTransactionArgs = [ + 'readOnly' => [ + 'minReadTimestamp' => $ts, + 'readTimestamp' => $ts + ] + ]; + + $roObjArgs = $readOnlyTransactionArgs; + $roObjArgs['readOnly']['minReadTimestamp'] = $this->formatTimestampForApi($ts); + $roObjArgs['readOnly']['readTimestamp'] = $this->formatTimestampForApi($ts); + $readOnlyTransactionOptions = new TransactionOptions; + $ro = (new TransactionOptions\ReadOnly) + ->deserialize($roObjArgs['readOnly'], $codec); + + $readOnlyTransactionOptions->setReadOnly($ro); + + $insertMutations = [ + [ + 'insert' => [ + 'table' => $tableName, + 'columns' => ['foo'], + 'values' => ['bar'] + ] + ], + // [ + // 'delete' => [ + // 'table' => $tableName, + // 'keySet' => [ + // 'keys' => ['foo','bar'], + // 'ranges' => [ + // [ + // 'startOpen' => ['foo'], + // 'endClosed' => ['bar'] + // ] + // ] + // ] + // ] + // ] + ]; + + $insertMutationsArr = []; + $insert = $insertMutations[0]['insert']; + $insert['values'] = $this->formatListForApi($insertMutations[0]['insert']['values']); + $operation = (new Mutation\Write) + ->deserialize($insert, $codec); + + $mutation = new Mutation; + $mutation->setInsert($operation); + $insertMutationsArr[] = $mutation; + + // $delete = $mutations[1]['delete']; + // $delete['keySet']['keys'] = $this->formatListForApi($delete['keySet']['keys']); + // $operation = (new Mutation\Delete) + // ->deserialize($delete, $codec); + // $mutation = new Mutation; + // $mutation->setDelete($operation); + // $mutationsArr[] = $mutation; + + return [ + [ + 'listInstanceConfigs', + ['projectId' => self::PROJECT], + [self::PROJECT, []] + ], + [ + 'getInstanceConfig', + ['name' => $configName], + [$configName, []] + ], + [ + 'listInstances', + ['projectId' => self::PROJECT], + [self::PROJECT, []] + ], + [ + 'getInstance', + ['name' => $instanceName], + [$instanceName, []] + ], + [ + 'createInstance', + ['projectId' => self::PROJECT, 'instanceId' => $instanceName] + $instanceArgs, + [self::PROJECT, $instanceName, $instance, []], + $lro, + null + ], + [ + 'updateInstance', + $instanceArgs, + [$instance, $fieldMask, []], + $lro, + null + ], + [ + 'deleteInstance', + ['name' => $instanceName], + [$instanceName, []] + ], + [ + 'setInstanceIamPolicy', + ['resource' => $instanceName, 'policy' => $policy], + [$instanceName, $policy, []] + ], + [ + 'getInstanceIamPolicy', + ['resource' => $instanceName], + [$instanceName, []] + ], + [ + 'testInstanceIamPermissions', + ['resource' => $instanceName, 'permissions' => $permissions], + [$instanceName, $permissions, []] + ], + [ + 'listDatabases', + ['instance' => $instanceName], + [$instanceName, []] + ], + [ + 'createDatabase', + ['instance' => $instanceName, 'createStatement' => $createStmt, 'extraStatements' => []], + [$instanceName, $createStmt, [], []], + $lro, + null + ], + [ + 'updateDatabaseDdl', + ['name' => $databaseName, 'statements' => []], + [$databaseName, [], []], + $lro, + null + ], + [ + 'dropDatabase', + ['name' => $databaseName], + [$databaseName, []] + ], + [ + 'getDatabaseDDL', + ['name' => $databaseName], + [$databaseName, []] + ], + [ + 'setDatabaseIamPolicy', + ['resource' => $databaseName, 'policy' => $policy], + [$databaseName, $policy, []] + ], + [ + 'getDatabaseIamPolicy', + ['resource' => $databaseName], + [$databaseName, []] + ], + [ + 'testDatabaseIamPermissions', + ['resource' => $databaseName, 'permissions' => $permissions], + [$databaseName, $permissions, []] + ], + [ + 'createSession', + ['database' => $databaseName], + [$databaseName, []] + ], + [ + 'getSession', + ['name' => $sessionName], + [$sessionName, []] + ], + [ + 'deleteSession', + ['name' => $sessionName], + [$sessionName, []] + ], + [ + 'executeStreamingSql', + [ + 'session' => $sessionName, + 'sql' => $sql, + 'transactionId' => $transactionName + ] + $mapped, + [$sessionName, $sql, [ + 'transaction' => $transactionSelector, + 'params' => $expectedParams, + 'paramTypes' => $expectedParamTypes + ]] + ], + [ + 'streamingRead', + ['keySet' => [], 'transactionId' => $transactionName, 'session' => $sessionName, 'table' => $tableName, 'columns' => $columns], + [$sessionName, $tableName, $columns, $keySet, ['transaction' => $transactionSelector]] + ], + // test read write + [ + 'beginTransaction', + ['session' => $sessionName, 'transactionOptions' => $readWriteTransactionArgs], + [$sessionName, $readWriteTransactionOptions, []] + ], + // test read only + [ + 'beginTransaction', + ['session' => $sessionName, 'transactionOptions' => $readOnlyTransactionArgs], + [$sessionName, $readOnlyTransactionOptions, []] + ], + // test insert + [ + 'commit', + ['session' => $sessionName, 'mutations' => $insertMutations], + [$sessionName, $insertMutationsArr, []] + ], + // test single-use transaction + [ + 'commit', + ['session' => $sessionName, 'mutations' => [], 'singleUseTransaction' => true], + [$sessionName, [], ['singleUseTransaction' => $readWriteTransactionOptions]] + ], + [ + 'rollback', + ['session' => $sessionName, 'transactionId' => $transactionName], + [$sessionName, $transactionName, []] + ], + // ['getOperation'], + // ['cancelOperation'], + // ['deleteOperation'], + // ['listOperations'] + ]; + } +} diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php index 0dffb84bc936..f6dc5692973c 100644 --- a/tests/unit/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/DatabaseTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Core\Exception\AbortedException; use Google\Cloud\Core\Exception\NotFoundException; @@ -23,6 +23,7 @@ use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Duration; @@ -77,7 +78,7 @@ public function setUp() $this->sessionPool->release(Argument::type(Session::class)) ->willReturn(null); - $this->instance->name()->willReturn(self::INSTANCE); + $this->instance->name()->willReturn(InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE)); $args = [ $this->connection->reveal(), @@ -96,12 +97,55 @@ public function setUp() $this->database = \Google\Cloud\Dev\stub(Database::class, $args, $props); } + public function testName() + { + $this->assertEquals($this->database->name(), DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE)); + } + + public function testInfo() + { + $res = [ + 'name' => $this->database->name() + ]; + + $this->connection->getDatabase(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($res); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->assertEquals($res, $this->database->info()); + + // Make sure the request only is sent once. + $this->database->info(); + } + + public function testReload() + { + $res = [ + 'name' => $this->database->name() + ]; + + $this->connection->getDatabase(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn($res); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->assertEquals($res, $this->database->reload()); + + // Make sure the request is sent each time the method is called. + $this->database->reload(); + } + /** * @group spanneradmin */ public function testExists() { - $this->connection->getDatabaseDDL(Argument::any()) + $this->connection->getDatabase(Argument::withEntry( + 'name', DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + )) ->shouldBeCalled() ->willReturn([]); @@ -115,7 +159,7 @@ public function testExists() */ public function testExistsNotFound() { - $this->connection->getDatabaseDDL(Argument::any()) + $this->connection->getDatabase(Argument::any()) ->shouldBeCalled() ->willThrow(new NotFoundException('', 404)); @@ -130,9 +174,11 @@ public function testExistsNotFound() public function testUpdateDdl() { $statement = 'foo'; - $this->connection->updateDatabase([ + $this->connection->updateDatabaseDdl([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE), 'statements' => [$statement] + ])->willReturn([ + 'name' => 'my-operation' ]); $this->database->___setProperty('connection', $this->connection->reveal()); @@ -146,14 +192,16 @@ public function testUpdateDdl() public function testUpdateDdlBatch() { $statements = ['foo', 'bar']; - $this->connection->updateDatabase([ + $this->connection->updateDatabaseDdl([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE), 'statements' => $statements + ])->willReturn([ + 'name' => 'my-operation' ]); $this->database->___setProperty('connection', $this->connection->reveal()); - $this->database->updateDdl($statements); + $this->database->updateDdlBatch($statements); } /** @@ -162,7 +210,7 @@ public function testUpdateDdlBatch() public function testUpdateWithSingleStatement() { $statement = 'foo'; - $this->connection->updateDatabase([ + $this->connection->updateDatabaseDdl([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT, self::INSTANCE, self::DATABASE), 'statements' => ['foo'] ])->shouldBeCalled()->willReturn(['name' => 'operations/foo']); @@ -258,17 +306,40 @@ public function testRunTransaction() ->shouldBeCalled() ->willReturn(['id' => self::TRANSACTION]); + $this->connection->commit(Argument::any()) + ->shouldBeCalled() + ->willReturn(['commitTimestamp' => '2017-01-09T18:05:22.534799Z']); + $this->refreshOperation(); $hasTransaction = false; $this->database->runTransaction(function (Transaction $t) use (&$hasTransaction) { $hasTransaction = true; + + $t->commit(); }); $this->assertTrue($hasTransaction); } + /** + * @expectedException RuntimeException + */ + public function testRunTransactionNoCommit() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->connection->rollback(Argument::any()) + ->shouldBeCalled(); + + $this->refreshOperation(); + + $this->database->runTransaction(function (Transaction $t) {}); + } + public function testRunTransactionRetry() { $abort = new AbortedException('foo', 409, null, [ @@ -309,8 +380,8 @@ public function testRunTransactionAborted() $abort = new AbortedException('foo', 409, null, [ [ 'retryDelay' => [ - 'seconds' => 1, - 'nanos' => 0 + 'seconds' => 0, + 'nanos' => 500 ] ] ]); diff --git a/tests/unit/Spanner/DateTest.php b/tests/unit/Spanner/DateTest.php index 1c25c6bc3245..9ccb45b8d717 100644 --- a/tests/unit/Spanner/DateTest.php +++ b/tests/unit/Spanner/DateTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Date; @@ -33,6 +33,12 @@ public function setUp() $this->date = new Date($this->dt); } + public function testCreateFromValues() + { + $date = Date::createFromValues(1989,10,11); + $this->assertEquals($date->formatAsString(), $this->date->formatAsString()); + } + public function testGet() { $this->assertEquals($this->dt, $this->date->get()); diff --git a/tests/unit/Spanner/DurationTest.php b/tests/unit/Spanner/DurationTest.php index 37d2575e42f7..46c40d931260 100644 --- a/tests/unit/Spanner/DurationTest.php +++ b/tests/unit/Spanner/DurationTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Duration; diff --git a/tests/unit/Spanner/ConfigurationTest.php b/tests/unit/Spanner/InstanceConfigurationTest.php similarity index 79% rename from tests/unit/Spanner/ConfigurationTest.php rename to tests/unit/Spanner/InstanceConfigurationTest.php index 9cdceb8c336d..f8515a4a0d6c 100644 --- a/tests/unit/Spanner/ConfigurationTest.php +++ b/tests/unit/Spanner/InstanceConfigurationTest.php @@ -19,7 +19,7 @@ use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; -use Google\Cloud\Spanner\Configuration; +use Google\Cloud\Spanner\InstanceConfiguration; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Prophecy\Argument; @@ -27,7 +27,7 @@ * @group spanneradmin * @group spanner */ -class ConfigurationTest extends \PHPUnit_Framework_TestCase +class InstanceConfigurationTest extends \PHPUnit_Framework_TestCase { const PROJECT_ID = 'test-project'; const NAME = 'test-config'; @@ -38,7 +38,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->configuration = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->configuration = \Google\Cloud\Dev\stub(InstanceConfiguration::class, [ $this->connection->reveal(), self::PROJECT_ID, self::NAME @@ -47,16 +47,16 @@ public function setUp() public function testName() { - $this->assertEquals(self::NAME, $this->configuration->name()); + $this->assertEquals(self::NAME, InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($this->configuration->name())); } public function testInfo() { - $this->connection->getConfig(Argument::any())->shouldNotBeCalled(); + $this->connection->getInstanceConfig(Argument::any())->shouldNotBeCalled(); $this->configuration->___setProperty('connection', $this->connection->reveal()); $info = ['foo' => 'bar']; - $config = \Google\Cloud\Dev\stub(Configuration::class, [ + $config = \Google\Cloud\Dev\stub(InstanceConfiguration::class, [ $this->connection->reveal(), self::PROJECT_ID, self::NAME, @@ -70,7 +70,7 @@ public function testInfoWithReload() { $info = ['foo' => 'bar']; - $this->connection->getConfig([ + $this->connection->getInstanceConfig([ 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), 'projectId' => self::PROJECT_ID ])->shouldBeCalled()->willReturn($info); @@ -82,7 +82,7 @@ public function testInfoWithReload() public function testExists() { - $this->connection->getConfig(Argument::any())->willReturn([]); + $this->connection->getInstanceConfig(Argument::any())->willReturn([]); $this->configuration->___setProperty('connection', $this->connection->reveal()); $this->assertTrue($this->configuration->exists()); @@ -90,7 +90,7 @@ public function testExists() public function testExistsDoesntExist() { - $this->connection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404)); + $this->connection->getInstanceConfig(Argument::any())->willThrow(new NotFoundException('', 404)); $this->configuration->___setProperty('connection', $this->connection->reveal()); $this->assertFalse($this->configuration->exists()); @@ -100,7 +100,7 @@ public function testReload() { $info = ['foo' => 'bar']; - $this->connection->getConfig([ + $this->connection->getInstanceConfig([ 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), 'projectId' => self::PROJECT_ID ])->shouldBeCalledTimes(1)->willReturn($info); diff --git a/tests/unit/Spanner/InstanceTest.php b/tests/unit/Spanner/InstanceTest.php index 90533742c67d..7542546fafa7 100644 --- a/tests/unit/Spanner/InstanceTest.php +++ b/tests/unit/Spanner/InstanceTest.php @@ -60,7 +60,7 @@ public function setUp() public function testName() { - $this->assertEquals(self::NAME, $this->instance->name()); + $this->assertEquals(self::NAME, InstanceAdminClient::parseInstanceFromInstanceName($this->instance->name())); } public function testInfo() @@ -150,20 +150,16 @@ public function testUpdate() { $instance = $this->getDefaultInstance(); - $this->connection->getInstance(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - $this->connection->updateInstance([ + 'displayName' => 'bar', 'name' => $instance['name'], - 'displayName' => $instance['displayName'], - 'nodeCount' => $instance['nodeCount'], - 'labels' => [], - ])->shouldBeCalled(); + ])->shouldBeCalled()->willReturn([ + 'name' => 'my-operation' + ]); $this->instance->___setProperty('connection', $this->connection->reveal()); - $this->instance->update(); + $this->instance->update(['displayName' => 'bar']); } public function testUpdateWithExistingLabels() @@ -171,20 +167,16 @@ public function testUpdateWithExistingLabels() $instance = $this->getDefaultInstance(); $instance['labels'] = ['foo' => 'bar']; - $this->connection->getInstance(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - $this->connection->updateInstance([ - 'name' => $instance['name'], - 'displayName' => $instance['displayName'], - 'nodeCount' => $instance['nodeCount'], 'labels' => $instance['labels'], - ])->shouldBeCalled(); + 'name' => $instance['name'], + ])->shouldBeCalled()->willReturn([ + 'name' => 'my-operation' + ]); $this->instance->___setProperty('connection', $this->connection->reveal()); - $this->instance->update(); + $this->instance->update(['labels' => $instance['labels']]); } public function testUpdateWithChanges() @@ -199,16 +191,14 @@ public function testUpdateWithChanges() 'displayName' => 'New Name', ]; - $this->connection->getInstance(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - $this->connection->updateInstance([ 'name' => $instance['name'], 'displayName' => $changes['displayName'], 'nodeCount' => $changes['nodeCount'], 'labels' => $changes['labels'], - ])->shouldBeCalled(); + ])->shouldBeCalled()->willReturn([ + 'name' => 'my-operation' + ]); $this->instance->___setProperty('connection', $this->connection->reveal()); @@ -251,7 +241,7 @@ public function testDatabase() { $database = $this->instance->database('test-database'); $this->assertInstanceOf(Database::class, $database); - $this->assertEquals('test-database', $database->name()); + $this->assertEquals('test-database', DatabaseAdminClient::parseDatabaseFromDatabaseName($database->name())); } public function testDatabases() @@ -274,8 +264,8 @@ public function testDatabases() $dbs = iterator_to_array($dbs); $this->assertEquals(2, count($dbs)); - $this->assertEquals('database1', $dbs[0]->name()); - $this->assertEquals('database2', $dbs[1]->name()); + $this->assertEquals('database1', DatabaseAdminClient::parseDatabaseFromDatabaseName($dbs[0]->name())); + $this->assertEquals('database2', DatabaseAdminClient::parseDatabaseFromDatabaseName($dbs[1]->name())); } public function testDatabasesPaged() @@ -299,8 +289,8 @@ public function testDatabasesPaged() $dbs = iterator_to_array($dbs); $this->assertEquals(2, count($dbs)); - $this->assertEquals('database1', $dbs[0]->name()); - $this->assertEquals('database2', $dbs[1]->name()); + $this->assertEquals('database1', DatabaseAdminClient::parseDatabaseFromDatabaseName($dbs[0]->name())); + $this->assertEquals('database2', DatabaseAdminClient::parseDatabaseFromDatabaseName($dbs[1]->name())); } public function testIam() diff --git a/tests/unit/Spanner/KeyRangeTest.php b/tests/unit/Spanner/KeyRangeTest.php index f3903ce6aa7d..8d378ab3df4b 100644 --- a/tests/unit/Spanner/KeyRangeTest.php +++ b/tests/unit/Spanner/KeyRangeTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\KeyRange; @@ -31,6 +31,22 @@ public function setUp() $this->range = new KeyRange; } + public function testPrefixMatch() + { + $key = ['foo']; + + $range = new KeyRange([ + 'start' => $key, + 'end' => $key, + 'startType' => KeyRange::TYPE_CLOSED, + 'endType' => KeyRange::TYPE_CLOSED, + ]); + + $prefixRange = KeyRange::prefixMatch($key); + + $this->assertEquals($range, $prefixRange); + } + public function testGetters() { $range = new KeyRange([ diff --git a/tests/unit/Spanner/KeySetTest.php b/tests/unit/Spanner/KeySetTest.php index 75551a6dc946..4c13caddba0c 100644 --- a/tests/unit/Spanner/KeySetTest.php +++ b/tests/unit/Spanner/KeySetTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; @@ -87,7 +87,7 @@ public function testSetMatchAll() $this->assertTrue($set->keySetObject()['all']); $set->setMatchAll(false); - $this->assertFalse($set->keySetObject()['all']); + $this->assertFalse(isset($set->keySetObject()['all'])); } public function testRanges() @@ -116,4 +116,20 @@ public function testMatchAll() $set->setMatchAll(true); $this->assertTrue($set->matchAll()); } + + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidKeys() + { + new KeySet(['keys' => 'foo']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testInvalidAll() + { + new KeySet(['all' => 1]); + } } diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php index e111d718959f..af4f1e8b1da6 100644 --- a/tests/unit/Spanner/OperationTest.php +++ b/tests/unit/Spanner/OperationTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\KeyRange; @@ -269,9 +269,23 @@ public function testSnapshot() $snap = $this->operation->snapshot($this->session); $this->assertInstanceOf(Snapshot::class, $snap); + $this->assertEquals(Snapshot::TYPE_PRE_ALLOCATED, $snap->type()); $this->assertEquals(self::TRANSACTION, $snap->id()); } + public function testSnapshotSingleUse() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotBeCalled(); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snap = $this->operation->snapshot($this->session, ['singleUse' => true]); + $this->assertInstanceOf(Snapshot::class, $snap); + $this->assertEquals(Snapshot::TYPE_SINGLE_USE, $snap->type()); + $this->assertNull($snap->id()); + } + public function testSnapshotWithTimestamp() { $this->connection->beginTransaction(Argument::any()) diff --git a/tests/unit/Spanner/ResultTest.php b/tests/unit/Spanner/ResultTest.php index f61fe1d08cc0..173468c00ada 100644 --- a/tests/unit/Spanner/ResultTest.php +++ b/tests/unit/Spanner/ResultTest.php @@ -15,21 +15,18 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; -use Google\Cloud\Spanner\Operation; -use Google\Cloud\Spanner\Result; -use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Snapshot; use Google\Cloud\Spanner\Transaction; -use Google\Cloud\Spanner\ValueMapper; -use Prophecy\Argument; /** * @group spanner */ class ResultTest extends \PHPUnit_Framework_TestCase { + use ResultTestTrait; + /** * @dataProvider streamingDataProvider */ @@ -40,13 +37,6 @@ public function testRows($chunks, $expectedValues) $this->assertEquals($expectedValues, $result); } - public function streamingDataProvider() - { - foreach ($this->getStreamingDataFixture()['tests'] as $test) { - yield [$test['chunks'], $test['result']['value']]; - } - } - public function testIterator() { $fixture = $this->getStreamingDataFixture()['tests'][0]; @@ -96,56 +86,4 @@ public function testSnapshot() $result->rows()->next(); $this->assertInstanceOf(Snapshot::class, $result->snapshot()); } - - private function getResultClass($chunks, $context = 'r') - { - $operation = $this->prophesize(Operation::class); - $session = $this->prophesize(Session::class)->reveal(); - $mapper = $this->prophesize(ValueMapper::class); - $transaction = $this->prophesize(Transaction::class); - $snapshot = $this->prophesize(Snapshot::class); - $mapper->decodeValues( - Argument::any(), - Argument::any() - )->will(function ($args) { - return $args[1]; - }); - - if ($context === 'r') { - $operation->createSnapshot( - $session, - Argument::type('array') - )->willReturn($snapshot->reveal()); - } else { - $operation->createTransaction( - $session, - Argument::type('array') - )->willReturn($transaction->reveal()); - } - - return new Result( - $operation->reveal(), - $session, - function () use ($chunks) { - return $this->resultGenerator($chunks); - }, - $context, - $mapper->reveal() - ); - } - - private function resultGenerator($chunks) - { - foreach ($chunks as $chunk) { - yield json_decode($chunk, true); - } - } - - private function getStreamingDataFixture() - { - return json_decode( - file_get_contents(__DIR__ .'/../fixtures/spanner/streaming-read-acceptance-test.json'), - true - ); - } } diff --git a/tests/unit/Spanner/ResultTestTrait.php b/tests/unit/Spanner/ResultTestTrait.php new file mode 100644 index 000000000000..b40d1d270cdc --- /dev/null +++ b/tests/unit/Spanner/ResultTestTrait.php @@ -0,0 +1,96 @@ +getStreamingDataFixture()['tests'] as $test) { + yield [$test['chunks'], $test['result']['value']]; + } + } + + public function streamingDataProviderFirstChunk() + { + foreach ($this->getStreamingDataFixture()['tests'] as $test) { + yield [$test['chunks'], $test['result']['value']]; + break; + } + } + + private function getResultClass($chunks, $context = 'r') + { + $operation = $this->prophesize(Operation::class); + $session = $this->prophesize(Session::class)->reveal(); + $mapper = $this->prophesize(ValueMapper::class); + $transaction = $this->prophesize(Transaction::class); + $snapshot = $this->prophesize(Snapshot::class); + $mapper->decodeValues( + Argument::any(), + Argument::any() + )->will(function ($args) { + return $args[1]; + }); + + if ($context === 'r') { + $operation->createSnapshot( + $session, + Argument::type('array') + )->willReturn($snapshot->reveal()); + } else { + $operation->createTransaction( + $session, + Argument::type('array') + )->willReturn($transaction->reveal()); + } + + return new Result( + $operation->reveal(), + $session, + function () use ($chunks) { + return $this->resultGenerator($chunks); + }, + $context, + $mapper->reveal() + ); + } + + private function resultGenerator($chunks) + { + foreach ($chunks as $chunk) { + yield json_decode($chunk, true); + } + } + + private function getStreamingDataFixture() + { + return json_decode( + file_get_contents(__DIR__ .'/../fixtures/spanner/streaming-read-acceptance-test.json'), + true + ); + } +} diff --git a/tests/unit/Spanner/Session/CacheSessionPoolTest.php b/tests/unit/Spanner/Session/CacheSessionPoolTest.php index 47a494536233..8391af10b88e 100644 --- a/tests/unit/Spanner/Session/CacheSessionPoolTest.php +++ b/tests/unit/Spanner/Session/CacheSessionPoolTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner\Session; +namespace Google\Cloud\Tests\Unit\Spanner\Session; use Google\Auth\Cache\MemoryCacheItemPool; use Google\Cloud\Spanner\Database; diff --git a/tests/unit/Spanner/SnapshotTest.php b/tests/unit/Spanner/SnapshotTest.php index cf54179ce9ed..e846882c5f9a 100644 --- a/tests/unit/Spanner/SnapshotTest.php +++ b/tests/unit/Spanner/SnapshotTest.php @@ -15,12 +15,13 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Snapshot; use Google\Cloud\Spanner\Timestamp; +use Prophecy\Argument; /** * @group spanner @@ -33,16 +34,70 @@ class SnapshotTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->timestamp = new Timestamp(new \DateTime); + + $args = [ + 'id' => 'foo', + 'readTimestamp' => $this->timestamp + ]; + $this->snapshot = new Snapshot( $this->prophesize(Operation::class)->reveal(), $this->prophesize(Session::class)->reveal(), - 'foo', - $this->timestamp + $args + ); + } + + public function testTypeIsPreAllocated() + { + $this->assertEquals(Snapshot::TYPE_PRE_ALLOCATED, $this->snapshot->type()); + } + + public function testTypeIsSingleUse() + { + $snapshot = new Snapshot( + $this->prophesize(Operation::class)->reveal(), + $this->prophesize(Session::class)->reveal() ); + + $this->assertEquals(Snapshot::TYPE_SINGLE_USE, $snapshot->type()); } public function testReadTimestamp() { $this->assertEquals($this->timestamp, $this->snapshot->readTimestamp()); } + + /** + * @expectedException InvalidArgumentException + */ + public function testWithInvalidTimestamp() + { + $args = [ + 'readTimestamp' => 'foo' + ]; + + new Snapshot( + $this->prophesize(Operation::class)->reveal(), + $this->prophesize(Session::class)->reveal(), + $args + ); + } + + /** + * @expectedException BadMethodCallException + */ + public function testSingleUseFailsOnSecondUse() + { + $operation = $this->prophesize(Operation::class); + $operation->execute(Argument::any(), Argument::any(), Argument::any()) + ->shouldBeCalled(); + + $snapshot = new Snapshot( + $operation->reveal(), + $this->prophesize(Session::class)->reveal() + ); + + $snapshot->execute('foo'); + $snapshot->execute('foo'); + } } diff --git a/tests/unit/Spanner/SpannerClientTest.php b/tests/unit/Spanner/SpannerClientTest.php index e7006cbe2a68..c3c9937bff9a 100644 --- a/tests/unit/Spanner/SpannerClientTest.php +++ b/tests/unit/Spanner/SpannerClientTest.php @@ -15,18 +15,20 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Core\Int64; use Google\Cloud\Core\Iterator\ItemIterator; use Google\Cloud\Core\LongRunning\LongRunningOperation; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Bytes; -use Google\Cloud\Spanner\Configuration; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\InstanceConfiguration; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\SpannerClient; @@ -60,17 +62,17 @@ public function setUp() /** * @group spanneradmin */ - public function testConfigurations() + public function testInstanceConfigurations() { - $this->connection->listConfigs(Argument::any()) + $this->connection->listInstanceConfigs(Argument::any()) ->shouldBeCalled() ->willReturn([ 'instanceConfigs' => [ [ - 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG), 'displayName' => 'Bar' ], [ - 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG), 'displayName' => 'Bat' ] ] @@ -78,20 +80,20 @@ public function testConfigurations() $this->client->___setProperty('connection', $this->connection->reveal()); - $configs = $this->client->configurations(); + $configs = $this->client->instanceConfigurations(); $this->assertInstanceOf(ItemIterator::class, $configs); $configs = iterator_to_array($configs); $this->assertEquals(2, count($configs)); - $this->assertInstanceOf(Configuration::class, $configs[0]); - $this->assertInstanceOf(Configuration::class, $configs[1]); + $this->assertInstanceOf(InstanceConfiguration::class, $configs[0]); + $this->assertInstanceOf(InstanceConfiguration::class, $configs[1]); } /** * @group spanneradmin */ - public function testPagedConfigurations() + public function testPagedInstanceConfigurations() { $firstCall = [ 'instanceConfigs' => [ @@ -112,31 +114,31 @@ public function testPagedConfigurations() ] ]; - $this->connection->listConfigs(Argument::any()) + $this->connection->listInstanceConfigs(Argument::any()) ->shouldBeCalledTimes(2) ->willReturn($firstCall, $secondCall); $this->client->___setProperty('connection', $this->connection->reveal()); - $configs = $this->client->configurations(); + $configs = $this->client->instanceConfigurations(); $this->assertInstanceOf(ItemIterator::class, $configs); $configs = iterator_to_array($configs); $this->assertEquals(2, count($configs)); - $this->assertInstanceOf(Configuration::class, $configs[0]); - $this->assertInstanceOf(Configuration::class, $configs[1]); + $this->assertInstanceOf(InstanceConfiguration::class, $configs[0]); + $this->assertInstanceOf(InstanceConfiguration::class, $configs[1]); } /** * @group spanneradmin */ - public function testConfiguration() + public function testInstanceConfiguration() { - $config = $this->client->configuration('bar'); + $config = $this->client->instanceConfiguration('bar'); - $this->assertInstanceOf(Configuration::class, $config); - $this->assertEquals('bar', $config->name()); + $this->assertInstanceOf(InstanceConfiguration::class, $config); + $this->assertEquals('bar', InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config->name())); } /** @@ -145,8 +147,8 @@ public function testConfiguration() public function testCreateInstance() { $this->connection->createInstance(Argument::that(function ($arg) { - if ($arg['name'] !== 'projects/'. self::PROJECT .'/instances/'. self::INSTANCE) return false; - if ($arg['config'] !== 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG) return false; + if ($arg['name'] !== InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE)) return false; + if ($arg['config'] !== InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG)) return false; return true; })) @@ -157,8 +159,8 @@ public function testCreateInstance() $this->client->___setProperty('connection', $this->connection->reveal()); - $config = $this->prophesize(Configuration::class); - $config->name()->willReturn(self::CONFIG); + $config = $this->prophesize(InstanceConfiguration::class); + $config->name()->willReturn(InstanceAdminClient::formatInstanceConfigName(self::PROJECT, self::CONFIG)); $operation = $this->client->createInstance($config->reveal(), self::INSTANCE); @@ -172,7 +174,7 @@ public function testInstance() { $i = $this->client->instance('foo'); $this->assertInstanceOf(Instance::class, $i); - $this->assertEquals('foo', $i->name()); + $this->assertEquals('foo', InstanceAdminClient::parseInstanceFromInstanceName($i->name())); } /** @@ -205,8 +207,8 @@ public function testInstances() $instances = iterator_to_array($instances); $this->assertEquals(2, count($instances)); - $this->assertEquals('foo', $instances[0]->name()); - $this->assertEquals('bar', $instances[1]->name()); + $this->assertEquals('foo', InstanceAdminClient::parseInstanceFromInstanceName($instances[0]->name())); + $this->assertEquals('bar', InstanceAdminClient::parseInstanceFromInstanceName($instances[1]->name())); } /** @@ -225,7 +227,7 @@ public function testConnect() { $database = $this->client->connect(self::INSTANCE, self::DATABASE); $this->assertInstanceOf(Database::class, $database); - $this->assertEquals(self::DATABASE, $database->name()); + $this->assertEquals(self::DATABASE, DatabaseAdminClient::parseDatabaseFromDatabaseName($database->name())); } public function testConnectWithInstance() @@ -233,7 +235,7 @@ public function testConnectWithInstance() $inst = $this->client->instance(self::INSTANCE); $database = $this->client->connect($inst, self::DATABASE); $this->assertInstanceOf(Database::class, $database); - $this->assertEquals(self::DATABASE, $database->name()); + $this->assertEquals(self::DATABASE, DatabaseAdminClient::parseDatabaseFromDatabaseName($database->name())); } public function testKeyset() diff --git a/tests/unit/Spanner/TimestampTest.php b/tests/unit/Spanner/TimestampTest.php index c18ee94e95c2..62d35c94d8f7 100644 --- a/tests/unit/Spanner/TimestampTest.php +++ b/tests/unit/Spanner/TimestampTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Timestamp; diff --git a/tests/unit/Spanner/TransactionConfigurationTraitTest.php b/tests/unit/Spanner/TransactionConfigurationTraitTest.php index 2dd3f408e0b3..da1119bb9060 100644 --- a/tests/unit/Spanner/TransactionConfigurationTraitTest.php +++ b/tests/unit/Spanner/TransactionConfigurationTraitTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\Session\SessionPoolInterface; @@ -49,7 +49,7 @@ public function testTransactionSelectorBasicSnapshot() $args = []; $res = $this->impl->proxyTransactionSelector($args); $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); - $this->assertTrue($res[0]['singleUse']['readOnly']['strong']); + $this->assertEquals($res[0]['singleUse']['readOnly'], ['strong' => true]); } public function testTransactionSelectorExistingId() @@ -68,12 +68,19 @@ public function testTransactionSelectorReadWrite() $this->assertEquals($this->impl->proxyConfigureTransactionOptions(), $res[0]['singleUse']); } + public function testTransactionSelectorReadOnly() + { + $args = ['transactionType' => SessionPoolInterface::CONTEXT_READ]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + } + public function testBegin() { $args = ['begin' => true]; $res = $this->impl->proxyTransactionSelector($args); $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); - $this->assertTrue($res[0]['begin']['readOnly']['strong']); + $this->assertEquals($res[0]['begin']['readOnly'], ['strong' => true]); } public function testConfigureSnapshotOptionsReturnReadTimestamp() @@ -92,7 +99,7 @@ public function testConfigureSnapshotOptionsStrong() public function testConfigureSnapshotOptionsMinReadTimestamp() { - $args = ['minReadTimestamp' => $this->ts]; + $args = ['minReadTimestamp' => $this->ts, 'singleUse' => true]; $res = $this->impl->proxyConfigureSnapshotOptions($args); $this->assertEquals(self::TIMESTAMP, $res['readOnly']['minReadTimestamp']); } @@ -106,7 +113,7 @@ public function testConfigureSnapshotOptionsReadTimestamp() public function testConfigureSnapshotOptionsMaxStaleness() { - $args = ['maxStaleness' => $this->duration]; + $args = ['maxStaleness' => $this->duration, 'singleUse' => true]; $res = $this->impl->proxyConfigureSnapshotOptions($args); $this->assertEquals($this->dur, $res['readOnly']['maxStaleness']); } diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php index db5e71d9ad0d..cafceb6fc8fa 100644 --- a/tests/unit/Spanner/TransactionTest.php +++ b/tests/unit/Spanner/TransactionTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Instance; @@ -47,6 +47,9 @@ class TransactionTest extends \PHPUnit_Framework_TestCase private $session; private $database; + private $transaction; + private $singleUseTransaction; + public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); @@ -71,6 +74,9 @@ public function setUp() ]; $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props); + + unset($args[2]); + $this->singleUseTransaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props); } public function testInsert() @@ -168,7 +174,7 @@ public function testDelete() $mutations = $this->transaction->___getProperty('mutations'); $this->assertEquals('Posts', $mutations[0]['delete']['table']); $this->assertEquals('foo', $mutations[0]['delete']['keySet']['keys'][0]); - $this->assertFalse($mutations[0]['delete']['keySet']['all']); + $this->assertFalse(isset($mutations[0]['delete']['keySet']['all'])); } public function testExecute() @@ -228,7 +234,7 @@ public function testCommit() } /** - * @expectedException RuntimeException + * @expectedException BadMethodCallException */ public function testCommitInvalidState() { @@ -247,7 +253,7 @@ public function testRollback() } /** - * @expectedException RuntimeException + * @expectedException BadMethodCallException */ public function testRollbackInvalidState() { @@ -268,6 +274,14 @@ public function testState() $this->assertEquals(Transaction::STATE_COMMITTED, $this->transaction->state()); } + /** + * @expectedException BadMethodCallException + */ + public function testInvalidReadContext() + { + $this->singleUseTransaction->execute('foo'); + } + // ******* // Helpers diff --git a/tests/unit/Spanner/TransactionTypeTest.php b/tests/unit/Spanner/TransactionTypeTest.php new file mode 100644 index 000000000000..491449f1739b --- /dev/null +++ b/tests/unit/Spanner/TransactionTypeTest.php @@ -0,0 +1,777 @@ +timestamp = (new \DateTimeImmutable)->format(Timestamp::FORMAT); + + $this->connection = $this->prophesize(ConnectionInterface::class); + + $this->connection->createSession(Argument::any()) + ->willReturn(['name' => SpannerClient::formatSessionName( + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + )]); + } + + public function testDatabaseRunTransactionPreAllocate() + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if (!isset($arg['transactionOptions']['readWrite'])) return false; + if ($arg['singleUse']) return false; + + return true; + }))->shouldBeCalledTimes(1) + ->willReturn(['id' => self::TRANSACTION]); + + $this->connection->commit(Argument::withEntry('transactionId', self::TRANSACTION)) + ->shouldBeCalledTimes(1) + ->willReturn(['commitTimestamp' => $this->timestamp]); + + $database = $this->database($this->connection->reveal()); + + $database->runTransaction(function($t){ + $this->assertEquals($t->id(), self::TRANSACTION); + + $t->commit(); + }); + } + + public function testDatabaseRunTransactionSingleUse() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->commit(Argument::withEntry('singleUseTransaction', ['readWrite' => []])) + ->shouldBeCalledTimes(1) + ->willReturn(['commitTimestamp' => $this->timestamp]); + + $database = $this->database($this->connection->reveal()); + + $database->runTransaction(function($t){ + $this->assertNull($t->id()); + + $t->commit(); + }, ['singleUse' => true]); + } + + public function testDatabaseTransactionPreAllocate() + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if (!isset($arg['transactionOptions']['readWrite'])) return false; + if ($arg['singleUse']) return false; + + return true; + }))->shouldBeCalledTimes(1) + ->willReturn(['id' => self::TRANSACTION]); + + $database = $this->database($this->connection->reveal()); + + $transaction = $database->transaction(); + + $this->assertInstanceOf(Transaction::class, $transaction); + $this->assertEquals($transaction->id(), self::TRANSACTION); + } + + public function testDatabaseTransactionSingleUse() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $database = $this->database($this->connection->reveal()); + + $transaction = $database->transaction(['singleUse' => true]); + + $this->assertInstanceOf(Transaction::class, $transaction); + $this->assertNull($transaction->id()); + } + + public function testDatabaseSnapshotPreAllocate() + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if (!isset($arg['transactionOptions']['readOnly'])) return false; + if ($arg['singleUse']) return false; + + return true; + }))->shouldBeCalledTimes(1) + ->willReturn(['id' => self::TRANSACTION]); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot(); + + $this->assertInstanceOf(Snapshot::class, $snapshot); + $this->assertEquals($snapshot->id(), self::TRANSACTION); + } + + public function testDatabaseSnapshotSingleUse() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot(['singleUse' => true]); + + $this->assertInstanceOf(Snapshot::class, $snapshot); + $this->assertNull($snapshot->id()); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseSingleUseSnapshotMinReadTimestampAndMaxStaleness($chunks) + { + $seconds = 1; + $nanos = 2; + + $timestamp = new Timestamp(new \DateTimeImmutable($this->timestamp)); + $duration = new Duration($seconds, $nanos); + + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::that(function ($arg) use ($seconds, $nanos) { + $opts = $arg['transaction']['singleUse']['readOnly']; + if (isset($opts['strong'])) return false; + if ($opts['minReadTimestamp'] !== $this->timestamp) return false; + if ($opts['maxStaleness']['seconds'] !== $seconds) return false; + if ($opts['maxStaleness']['nanos'] !== $nanos) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'singleUse' => true, + 'minReadTimestamp' => $timestamp, + 'maxStaleness' => $duration + ]); + + $result = $snapshot->execute('SELECT * FROM Table')->rows()->current(); + } + + /** + * @expectedException BadMethodCallException + */ + public function testDatabasePreAllocatedSnapshotMinReadTimestamp() + { + $timestamp = new Timestamp(new \DateTimeImmutable($this->timestamp)); + + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::any()) + ->shouldNotbeCalled(); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'minReadTimestamp' => $timestamp, + ]); + } + + /** + * @expectedException BadMethodCallException + */ + public function testDatabasePreAllocatedSnapshotMaxStaleness() + { + $seconds = 1; + $nanos = 2; + + $duration = new Duration($seconds, $nanos); + + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::any()) + ->shouldNotbeCalled(); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'maxStaleness' => $duration + ]); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseSnapshotSingleUseReadTimestampAndExactStaleness($chunks) + { + $seconds = 1; + $nanos = 2; + + $timestamp = new Timestamp(new \DateTimeImmutable($this->timestamp)); + $duration = new Duration($seconds, $nanos); + + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::that(function ($arg) use ($seconds, $nanos) { + $opts = $arg['transaction']['singleUse']['readOnly']; + if (isset($opts['strong'])) return false; + if ($opts['readTimestamp'] !== $this->timestamp) return false; + if ($opts['exactStaleness']['seconds'] !== $seconds) return false; + if ($opts['exactStaleness']['nanos'] !== $nanos) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'singleUse' => true, + 'readTimestamp' => $timestamp, + 'exactStaleness' => $duration + ]); + + $snapshot->execute('SELECT * FROM Table')->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseSnapshotPreAllocateReadTimestampAndExactStaleness($chunks) + { + $seconds = 1; + $nanos = 2; + + $timestamp = new Timestamp(new \DateTimeImmutable($this->timestamp)); + $duration = new Duration($seconds, $nanos); + + $this->connection->beginTransaction(Argument::that(function ($arg) use ($seconds, $nanos) { + if ($arg['singleUse']) return false; + + $opts = $arg['transactionOptions']['readOnly']; + if (isset($opts['strong'])) return false; + if ($opts['readTimestamp'] !== $this->timestamp) return false; + if ($opts['exactStaleness']['seconds'] !== $seconds) return false; + if ($opts['exactStaleness']['nanos'] !== $nanos) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->connection->executeStreamingSql(Argument::withEntry('transaction', ['id' => self::TRANSACTION])) + ->shouldBeCalledTimes(1) + ->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'readTimestamp' => $timestamp, + 'exactStaleness' => $duration + ]); + + $snapshot->execute('SELECT * FROM Table')->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseSingleUseSnapshotStrongConsistency($chunks) + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::that(function ($arg) { + if (!$arg['transaction']['singleUse']['readOnly']['strong']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'singleUse' => true, + 'strong' => true + ]); + + $snapshot->execute('SELECT * FROM Table')->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabasePreAllocatedSnapshotStrongConsistency($chunks) + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if ($arg['singleUse']) return false; + + if (!$arg['transactionOptions']['readOnly']['strong']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->connection->executeStreamingSql(Argument::withEntry('transaction', ['id' => self::TRANSACTION])) + ->shouldBeCalledTimes(1) + ->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'strong' => true + ]); + + $snapshot->execute('SELECT * FROM Table')->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseSingleUseSnapshotDefaultsToStrongConsistency($chunks) + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::that(function ($arg) { + if (!$arg['transaction']['singleUse']['readOnly']['strong']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'singleUse' => true, + ]); + + $snapshot->execute('SELECT * FROM Table')->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabasePreAllocatedSnapshotDefaultsToStrongConsistency($chunks) + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if ($arg['singleUse']) return false; + + if (!$arg['transactionOptions']['readOnly']['strong']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->connection->executeStreamingSql(Argument::withEntry('transaction', ['id' => self::TRANSACTION])) + ->shouldBeCalledTimes(1) + ->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot(); + + $snapshot->execute('SELECT * FROM Table')->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseSnapshotReturnReadTimestamp($chunks) + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if ($arg['singleUse']) return false; + + if (!$arg['transactionOptions']['readOnly']['returnReadTimestamp']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->connection->executeStreamingSql(Argument::withEntry('transaction', ['id' => self::TRANSACTION])) + ->shouldBeCalledTimes(1) + ->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + + $snapshot = $database->snapshot([ + 'returnReadTimestamp' => true + ]); + + $snapshot->execute('SELECT * FROM Table')->rows()->current(); + } + + public function testDatabaseInsertSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->insert('Table', [ + 'column' => 'value' + ]); + } + + public function testDatabaseInsertBatchSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->insertBatch('Table', [[ + 'column' => 'value' + ]]); + } + + public function testDatabaseUpdateSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->update('Table', [ + 'column' => 'value' + ]); + } + + public function testDatabaseUpdateBatchSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->updateBatch('Table', [[ + 'column' => 'value' + ]]); + } + + public function testDatabaseInsertOrUpdateSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->insertOrUpdate('Table', [ + 'column' => 'value' + ]); + } + + public function testDatabaseInsertOrUpdateBatchSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->insertOrUpdateBatch('Table', [[ + 'column' => 'value' + ]]); + } + + public function testDatabaseReplaceSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->replace('Table', [ + 'column' => 'value' + ]); + } + + public function testDatabaseReplaceBatchSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->replaceBatch('Table', [[ + 'column' => 'value' + ]]); + } + + public function testDatabaseDeleteSingleUseReadWrite() + { + $this->connection->commit(Argument::that(function ($arg) { + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'commitTimestamp' => $this->timestamp + ]); + + $database = $this->database($this->connection->reveal()); + + $database->delete('Table', new KeySet); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseExecuteSingleUseReadOnly($chunks) + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::that(function ($arg) { + if (!$arg['transaction']['singleUse']['readOnly']['strong']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + $database->execute('SELECT * FROM Table')->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseExecuteBeginReadOnly($chunks) + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::that(function ($arg) { + if (!$arg['transaction']['begin']['readOnly']['strong']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + $database->execute('SELECT * FROM Table', [ + 'begin' => true + ])->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseExecuteBeginReadWrite($chunks) + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->executeStreamingSql(Argument::that(function ($arg) { + if ($arg['transaction']['begin']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + $database->execute('SELECT * FROM Table', [ + 'begin' => true, + 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE + ])->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseReadSingleUseReadOnly($chunks) + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->streamingRead(Argument::that(function ($arg) { + if (!$arg['transaction']['singleUse']['readOnly']['strong']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + $database->read('Table', new KeySet, [])->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseReadBeginReadOnly($chunks) + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->streamingRead(Argument::that(function ($arg) { + if (!$arg['transaction']['begin']['readOnly']['strong']) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + $database->read('Table', new KeySet, [], [ + 'begin' => true + ])->rows()->current(); + } + + /** + * @dataProvider streamingDataProviderFirstChunk + */ + public function testDatabaseReadBeginReadWrite($chunks) + { + $this->connection->beginTransaction(Argument::any()) + ->shouldNotbeCalled(); + + $this->connection->streamingRead(Argument::that(function ($arg) { + if ($arg['transaction']['begin']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + + $database = $this->database($this->connection->reveal()); + $database->read('Table', new KeySet, [], [ + 'begin' => true, + 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE + ])->rows()->current(); + } + + public function testTransactionPreAllocatedRollback() + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if (!isset($arg['transactionOptions']['readWrite'])) return false; + + return true; + }))->shouldBeCalledTimes(1)->willReturn(['id' => self::TRANSACTION]); + + $this->connection->rollback(Argument::that(function ($arg) { + if ($arg['transactionId'] !== self::TRANSACTION) return false; + if ($arg['session'] !== SpannerClient::formatSessionName( + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + )) return false; + + return true; + }))->shouldBeCalled(); + + $database = $this->database($this->connection->reveal()); + $t = $database->transaction(); + $t->rollback(); + } + + /** + * @expectedException BadMethodCallException + */ + public function testTransactionSingleUseRollback() + { + $this->connection->beginTransaction(Argument::any())->shouldNotbeCalled(); + $this->connection->rollback(Argument::any())->shouldNotbeCalled(); + + $database = $this->database($this->connection->reveal()); + $t = $database->transaction(['singleUse' => true]); + $t->rollback(); + } + + private function database(ConnectionInterface $connection) + { + $operation = new Operation($connection, false); + $instance = $this->prophesize(Instance::class); + $instance->name()->willReturn(InstanceAdminClient::formatInstanceName(self::PROJECT, self::INSTANCE)); + + $database = \Google\Cloud\Dev\stub(Database::class, [ + $connection, + $instance->reveal(), + $this->prophesize(LongRunningConnectionInterface::class)->reveal(), + [], + self::PROJECT, + self::DATABASE + ], ['operation']); + + $database->___setProperty('operation', $operation); + + return $database; + } +} diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php index 404ba6a3201e..d5d83eabf9ea 100644 --- a/tests/unit/Spanner/ValueMapperTest.php +++ b/tests/unit/Spanner/ValueMapperTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Core\Int64; use Google\Cloud\Spanner\Bytes; @@ -86,7 +86,7 @@ public function testFormatParamsForExecuteSqlArray() } /** - * @expectedException InvalidArgumentException + * @expectedException BadMethodCallException */ public function testFormatParamsForExecuteSqlArrayInvalidAssoc() { @@ -96,7 +96,7 @@ public function testFormatParamsForExecuteSqlArrayInvalidAssoc() } /** - * @expectedException InvalidArgumentException + * @expectedException BadMethodCallException */ public function testFormatParamsForExecuteSqlInvalidTypes() { From 097170fd1bf4a8b52bd091b8c1ecf774d5f65684 Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Fri, 5 May 2017 15:51:29 -0400 Subject: [PATCH 08/46] Buffer result sets and retry only with error code unavailable --- src/Spanner/Connection/Grpc.php | 2 - src/Spanner/Database.php | 8 +- src/Spanner/Result.php | 226 +++++++++++++++-------- src/Spanner/Session/CacheSessionPool.php | 7 +- tests/snippets/Spanner/DatabaseTest.php | 3 +- tests/snippets/Trace/TraceClientTest.php | 4 +- tests/system/Spanner/TransactionTest.php | 13 +- 7 files changed, 161 insertions(+), 102 deletions(-) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index a02d4f179364..b78a98cc22e3 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -414,10 +414,8 @@ public function executeStreamingSql(array $args) { $params = $this->pluck('params', $args); if ($params) { - // print_r($this->formatStructForApi($params)); $args['params'] = (new protobuf\Struct) ->deserialize($this->formatStructForApi($params), $this->codec); - // var_dump($args['params']);exit; } if (isset($args['paramTypes']) && is_array($args['paramTypes'])) { diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index e0099b7bb129..9f24360819ab 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -1303,8 +1303,8 @@ public function __destruct() /** * Create a new session. * - * Sessions are handled behind the scenes and this method not need to be - * called directly. + * Sessions are handled behind the scenes and this method does not need to + * be called directly. * * @access private * @param array $options [optional] Configuration options. @@ -1324,8 +1324,8 @@ public function createSession(array $options = []) * point. To see the operations that can be performed on a session please * see {@see Google\Cloud\Spanner\Session\Session}. * - * Sessions are handled behind the scenes and this method not need to be - * called directly. + * Sessions are handled behind the scenes and this method does not need to + * be called directly. * * @access private * @param string $name The session's name. diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index 727f2e6fe3b8..3e68ce94def5 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -17,7 +17,9 @@ namespace Google\Cloud\Spanner; +use Grpc; use Google\Cloud\Core\Exception\ServiceException; +use Google\Cloud\Core\ExponentialBackoff; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\Timestamp; @@ -39,16 +41,18 @@ */ class Result implements \IteratorAggregate { - /** - * @var array|null - */ - private $cachedValues; + const BUFFER_RESULT_LIMIT = 10; /** * @var array */ private $columns = []; + /** + * @var int + */ + private $columnCount; + /** * @var ValueMapper */ @@ -64,6 +68,11 @@ class Result implements \IteratorAggregate */ private $operation; + /** + * @var int + */ + private $retries; + /** * @var string|null */ @@ -100,25 +109,29 @@ class Result implements \IteratorAggregate * @param \Generator $resultGenerator Reads rows from Google Cloud Spanner. * @param string $transactionContext The transaction's context. * @param ValueMapper $mapper Maps values. + * @param int $retries Number of attempts to resume a broken stream, assuming + * a resume token is present. **Defaults to** 3. */ public function __construct( Operation $operation, Session $session, callable $call, $transactionContext, - ValueMapper $mapper + ValueMapper $mapper, + $retries = 3 ) { $this->operation = $operation; $this->session = $session; $this->call = $call; $this->transactionContext = $transactionContext; $this->mapper = $mapper; + $this->retries = $retries; } /** - * Return the formatted and decoded rows. - * - * If the stream is interrupted an attempt will be made to resume. + * Return the formatted and decoded rows. If the stream is interrupted and + * a resume token is available, attempts will be made on your behalf to + * resume. * * Example: * ``` @@ -129,20 +142,67 @@ public function __construct( */ public function rows() { + $bufferedResults = []; $call = $this->call; + $generator = $call(); + $shouldRetry = false; + + while ($generator->valid()) { + try { + $result = $generator->current(); + $bufferedResults[] = $result; + $this->setResultData($result); + + if (!isset($result['values'])) { + return; + } + + if (isset($result['resumeToken']) || count($bufferedResults) >= self::BUFFER_RESULT_LIMIT) { + list($yieldableRows, $chunkedResult) = $this->parseRowsFromBufferedResults($bufferedResults); + + foreach ($yieldableRows as $row) { + yield $this->mapper->decodeValues($this->columns, $row); + } + + // Now that we've yielded all available rows, flush the buffer. + $bufferedResults = []; + $shouldRetry = isset($result['resumeToken']) + ? true + : false; + + // If the last item in the buffer had a chunked value let's + // hold on to it so we can stitch it together into a yieldable + // result. + if ($chunkedResult) { + $bufferedResults[] = $chunkedResult; + } + } + + $generator->next(); + } catch (\Exception $ex) { + if ($shouldRetry && $ex->getCode() === Grpc\STATUS_UNAVAILABLE) { + $backoff = new ExponentialBackoff($this->retries, function (\Exception $ex) { + return $ex->getCode() === Grpc\STATUS_UNAVAILABLE + ? true + : false; + }); + + // Attempt to resume using our last stored resume token. If we + // successfully resume, flush the buffer. + $generator = $backoff->execute($call, [$this->resumeToken]); + $bufferedResults = []; + } - try { - foreach ($this->getRows($call()) as $row) { - yield $row; - } - } catch (ServiceException $ex) { - if (!$this->resumeToken) { throw $ex; } + } - // If we have a token, attempt to resume - foreach ($this->getRows($call($this->resumeToken)) as $row) { - yield $row; + // If there are any results remaining in the buffer, yield them. + if ($bufferedResults) { + list($yieldableRows, $chunkedResult) = $this->parseRowsFromBufferedResults($bufferedResults); + + foreach ($yieldableRows as $row) { + yield $this->mapper->decodeValues($this->columns, $row); } } } @@ -230,6 +290,7 @@ public function transaction() /** * @access private + * @return \Generator */ public function getIterator() { @@ -237,91 +298,100 @@ public function getIterator() } /** - * Yields rows from a partial result set. - * - * @return \Generator + * @param array $bufferedResults + * @return array */ - private function getRowsFromPartial(array $partial) + private function parseRowsFromBufferedResults(array $bufferedResults) { - $this->stats = isset($partial['stats']) ? $partial['stats'] : null; - $this->resumeToken = isset($partial['resumeToken']) ? $partial['resumeToken'] : null; + $values = []; + $chunkedResult = null; + $shouldMergeValues = isset($bufferedResults[0]['chunkedValue']); + + foreach ($bufferedResults as $key => $result) { + if ($key === 0) { + $values = $bufferedResults[0]['values']; + continue; + } + + $values = $shouldMergeValues + ? $this->mergeValues($values, $result['values']) + : array_merge($values, $result['values']); + $shouldMergeValues = (isset($result['chunkedValue'])) + ? true + : false; + } + + $yieldableRows = array_chunk($values, $this->columnCount); - if (isset($partial['metadata'])) { - $this->metadata = $partial['metadata']; - $this->columns = $partial['metadata']['rowType']['fields']; + if (isset($result['chunkedValue'])) { + $chunkedResult = [ + 'values' => array_pop($yieldableRows), + 'chunkedValue' => true + ]; } - if (isset($partial['metadata']['transaction']['id'])) { + return [ + $yieldableRows, + $chunkedResult + ]; + } + + /** + * @param array $result + */ + private function setResultData(array $result) + { + $this->stats = isset($result['stats']) + ? $result['stats'] + : null; + + if (isset($result['resumeToken'])) { + $this->resumeToken = $result['resumeToken']; + } + + if (isset($result['metadata'])) { + $this->metadata = $result['metadata']; + $this->columns = $result['metadata']['rowType']['fields']; + $this->columnCount = count($this->columns); + } + + if (isset($result['metadata']['transaction']['id'])) { if ($this->transactionContext === SessionPoolInterface::CONTEXT_READ) { $this->snapshot = $this->operation->createSnapshot( $this->session, - $partial['metadata']['transaction'] + $result['metadata']['transaction'] ); } else { $this->transaction = $this->operation->createTransaction( $this->session, - $partial['metadata']['transaction'] + $result['metadata']['transaction'] ); } } - - if ($this->cachedValues) { - $partial['values'] = $this->mergeValues($this->cachedValues, $partial['values']); - $this->cachedValues = null; - } - - if (isset($partial['chunkedValue'])) { - $this->cachedValues = $partial['values']; - return; - } - - $rows = []; - $columnCount = count($this->columns); - - if ($columnCount > 0 && isset($partial['values'])) { - $rows = array_chunk($partial['values'], $columnCount); - } - - foreach ($rows as $row) { - yield $this->mapper->decodeValues($this->columns, $row); - } } /** * Merge result set values together. * - * @param array $cached - * @param array $new - * @return mixed + * @param array $set1 + * @param array $set2 + * @return array */ - private function mergeValues(array $cached, array $new) + private function mergeValues(array $set1, array $set2) { - $lastCachedItem = array_pop($cached); - $firstNewItem = array_shift($new); - $item = $firstNewItem; - - if (is_string($lastCachedItem) && is_string($firstNewItem)) { - $item = $lastCachedItem . $firstNewItem; - } elseif (is_array($lastCachedItem)) { - $item = $this->mergeValues($lastCachedItem, $firstNewItem); + $lastItemSet1 = array_pop($set1); + $firstItemSet2 = array_shift($set2); + $item = $firstItemSet2; + + if (is_string($lastItemSet1) && is_string($firstItemSet2)) { + $item = $lastItemSet1 . $firstItemSet2; + } elseif (is_array($lastItemSet1)) { + $item = $this->mergeValues($lastItemSet1, $firstItemSet2); } else { - array_push($cached, $lastCachedItem); + array_push($set1, $lastItemSet1); } - array_push($cached, $item); - return array_merge($cached, $new); - } - - /** - * @param \Generator $results - * @return \Generator - */ - private function getRows(\Generator $results) - { - foreach ($results as $partial) { - foreach ($this->getRowsFromPartial($partial) as $row) { - yield $row; - } - } + array_push($set1, $item); + return array_merge($set1, $set2); } } diff --git a/src/Spanner/Session/CacheSessionPool.php b/src/Spanner/Session/CacheSessionPool.php index 2d8702d2cb1e..b67737eec5b5 100644 --- a/src/Spanner/Session/CacheSessionPool.php +++ b/src/Spanner/Session/CacheSessionPool.php @@ -26,7 +26,7 @@ /** * This session pool implementation accepts a PSR-6 compatible cache - * implementation and utilizies it to store sessions between requests. + * implementation and utilizes it to store sessions between requests. * * Please note that if you configure a high minimum session value the first * request and any after a period of inactivity greater than an hour (the point @@ -65,7 +65,7 @@ class CacheSessionPool implements SessionPoolInterface * @var array */ private static $defaultConfig = [ - 'maxSessions' => PHP_INT_MAX, + 'maxSessions' => 500, 'minSessions' => 1, 'shouldWaitForSession' => true, 'maxCyclesToWaitForSession' => 30, @@ -252,7 +252,8 @@ public function release(Session $session) } /** - * Clear the session pool. + * Clear the session pool. Please note that this simply removes sessions + * data from the cache and does not delete the sessions themselves. */ public function clear() { diff --git a/tests/snippets/Spanner/DatabaseTest.php b/tests/snippets/Spanner/DatabaseTest.php index bbc7910928a9..6c7488a619b4 100644 --- a/tests/snippets/Spanner/DatabaseTest.php +++ b/tests/snippets/Spanner/DatabaseTest.php @@ -378,8 +378,7 @@ public function testRunTransactionRollback() 'rowType' => [ 'fields' => [] ] - ], - 'values' => [] + ] ])); $this->stubOperation(); diff --git a/tests/snippets/Trace/TraceClientTest.php b/tests/snippets/Trace/TraceClientTest.php index 6d4de01803e4..e38d76260b43 100644 --- a/tests/snippets/Trace/TraceClientTest.php +++ b/tests/snippets/Trace/TraceClientTest.php @@ -35,8 +35,8 @@ class TraceClientTest extends SnippetTestCase public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = new \TraceClientStub; - $this->client->setConnection($this->connection->reveal()); + $this->client = \Google\Cloud\Dev\stub(TraceClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testClass() diff --git a/tests/system/Spanner/TransactionTest.php b/tests/system/Spanner/TransactionTest.php index 66f3ff1613f0..b9494cb837e9 100644 --- a/tests/system/Spanner/TransactionTest.php +++ b/tests/system/Spanner/TransactionTest.php @@ -22,7 +22,7 @@ use Google\Cloud\Spanner\Timestamp; /** - * @group spannerz + * @group spanner */ class TransactionTest extends SpannerTestCase { @@ -37,7 +37,7 @@ public static function setUpBeforeClass() 'name' => uniqid(self::TESTING_PREFIX), 'birthday' => new Date(new \DateTime('2000-01-01')) ]; - echo 'inserting row'.PHP_EOL; + self::$database->insert(self::TEST_TABLE_NAME, self::$row); } @@ -100,16 +100,7 @@ public function testExactTimestampRead() list($keySet, $cols) = $this->readArgs(); - echo "Cached row data (should match snapshot result)".PHP_EOL; - print_r(self::$row); - $res = $snapshot->read(self::TEST_TABLE_NAME, $keySet, $cols)->rows(); - echo PHP_EOL."Snapshot Result". PHP_EOL; - print_r(iterator_to_array($res)); - - echo PHP_EOL."Database Result". PHP_EOL; - print_r(iterator_to_array($db->read(self::TEST_TABLE_NAME, $keySet, $cols)->rows())); - exit; $row = $res->current(); $this->assertEquals($ts->get(), $snapshot->readTimestamp()->get()); From 201e6b744459553a9c74073be24460024fc9a81b Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 8 May 2017 14:54:48 -0400 Subject: [PATCH 09/46] Fix parameterization and value mapping Add system test coverage. Add more read system tests and fix KeyRange issues Spanner transaction system tests Add remaining p0 spanner system tests --- src/Core/GrpcRequestWrapper.php | 23 +- src/Core/GrpcTrait.php | 2 +- src/Core/PhpArray.php | 6 +- src/Spanner/Connection/Grpc.php | 125 ++++++- src/Spanner/Database.php | 17 +- src/Spanner/KeyRange.php | 46 ++- src/Spanner/Operation.php | 39 ++- src/Spanner/Transaction.php | 45 ++- src/Spanner/TransactionalReadTrait.php | 13 + src/Spanner/V1/SpannerClient.php | 2 +- src/Spanner/ValueMapper.php | 63 +++- tests/system/Spanner/ReadTest.php | 389 +++++++++++++++++++++ tests/system/Spanner/SnapshotTest.php | 189 +++++++++- tests/system/Spanner/SpannerTestCase.php | 7 + tests/system/Spanner/TransactionTest.php | 209 +++++++++-- tests/system/Spanner/WriteTest.php | 375 ++++++++++++++++++++ tests/unit/Spanner/Connection/GrpcTest.php | 34 +- tests/unit/Spanner/DatabaseTest.php | 12 +- tests/unit/Spanner/KeyRangeTest.php | 2 +- tests/unit/Spanner/TransactionTest.php | 19 + tests/unit/Spanner/ValueMapperTest.php | 3 +- 21 files changed, 1451 insertions(+), 169 deletions(-) create mode 100644 tests/system/Spanner/ReadTest.php create mode 100644 tests/system/Spanner/WriteTest.php diff --git a/src/Core/GrpcRequestWrapper.php b/src/Core/GrpcRequestWrapper.php index ffe5066121ac..3d89d417a9e0 100644 --- a/src/Core/GrpcRequestWrapper.php +++ b/src/Core/GrpcRequestWrapper.php @@ -170,7 +170,7 @@ private function handleResponse($response) if ($response instanceof Message) { $res = $response->serialize($this->codec); - return $this->convertNulls($res); + return $res; } if ($response instanceof OperationResponse) { @@ -195,7 +195,7 @@ private function handleStream(ServerStream $response) try { foreach ($response->readAll() as $count => $result) { $res = $result->serialize($this->codec); - yield $this->convertNulls($res); + yield $res; } } catch (\Exception $ex) { throw $this->convertToGoogleException($ex); @@ -259,23 +259,4 @@ private function convertToGoogleException(ApiException $ex) return new $exception($ex->getMessage(), $ex->getCode(), $ex, $metadata); } - - /** - * Convert NullValue types to PHP null. - * - * @param array [ref] $result - * @return array - */ - private function convertNulls(array &$result) - { - foreach ($result as $key => $value) { - if (is_array($value) && array_key_exists('nullValue', $value) && $value['nullValue'][0] === null) { - $result[$key] = null; - } elseif (is_array($value)) { - $result[$key] = $this->convertNulls($value); - } - } - - return $result; - } } diff --git a/src/Core/GrpcTrait.php b/src/Core/GrpcTrait.php index 9867f26d8803..46313151a373 100644 --- a/src/Core/GrpcTrait.php +++ b/src/Core/GrpcTrait.php @@ -181,7 +181,7 @@ private function formatValueForApi($value) case 'NULL': return ['null_value' => protobuf\NullValue::NULL_VALUE]; case 'array': - if ($this->isAssoc($value)) { + if (!empty($value) && $this->isAssoc($value)) { return ['struct_value' => $this->formatStructForApi($value)]; } diff --git a/src/Core/PhpArray.php b/src/Core/PhpArray.php index cf8a5fd360b7..882b2da72f5d 100644 --- a/src/Core/PhpArray.php +++ b/src/Core/PhpArray.php @@ -159,7 +159,7 @@ protected function decodeMessage(Protobuf\Message $message, $data) protected function filterValue($value, Protobuf\Field $field) { if (trim($field->getReference(), '\\') === NullValue::class) { - return 0; + return null; } if ($value instanceof Protobuf\Message) { @@ -195,7 +195,7 @@ protected function filterValue($value, Protobuf\Field $field) foreach ($fields as $field) { $name = $field->getName(); - if ($val->$name) { + if ($val->$name !== null) { $vals[] = $this->filterValue($val->$name, $field); } } @@ -209,7 +209,7 @@ protected function filterValue($value, Protobuf\Field $field) foreach ($fields as $field) { $name = $field->getName(); - if ($value->$name) { + if ($value->$name !== null) { return $this->filterValue($value->$name, $field); } } diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index b78a98cc22e3..d5a5f9b9ca22 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -414,8 +414,19 @@ public function executeStreamingSql(array $args) { $params = $this->pluck('params', $args); if ($params) { - $args['params'] = (new protobuf\Struct) - ->deserialize($this->formatStructForApi($params), $this->codec); + $args['params'] = []; + + $args['params'] = new protobuf\Struct; + + foreach ($params as $key => $param) { + $field = $this->fieldValue($param); + + $fields = new protobuf\Struct\FieldsEntry; + $fields->setKey($key); + $fields->setValue($field); + + $args['params']->addFields($fields); + } } if (isset($args['paramTypes']) && is_array($args['paramTypes'])) { @@ -462,19 +473,10 @@ public function beginTransaction(array $args) { $options = new TransactionOptions; - $transactionOptions = $this->pluck('transactionOptions', $args); + $transactionOptions = $this->formatTransactionOptions($this->pluck('transactionOptions', $args)); if (isset($transactionOptions['readOnly'])) { - $ro = $transactionOptions['readOnly']; - if (isset($ro['minReadTimestamp'])) { - $ro['minReadTimestamp'] = $this->formatTimestampForApi($ro['minReadTimestamp']); - } - - if (isset($ro['readTimestamp'])) { - $ro['readTimestamp'] = $this->formatTimestampForApi($ro['readTimestamp']); - } - $readOnly = (new TransactionOptions\ReadOnly) - ->deserialize($ro, $this->codec); + ->deserialize($transactionOptions['readOnly'], $this->codec); $options->setReadOnly($readOnly); } else { @@ -513,10 +515,18 @@ public function commit(array $args) break; default: - $data['values'] = $this->formatListForApi($data['values']); + $operation = new Mutation\Write; + $operation->setTable($data['table']); + $operation->setColumns($data['columns']); - $operation = (new Mutation\Write) - ->deserialize($data, $this->codec); + $list = new protobuf\ListValue; + foreach ($data['values'] as $key => $param) { + $field = $this->fieldValue($param); + + $list->addValues($field); + } + + $operation->setValues($list); break; } @@ -621,7 +631,11 @@ private function formatKeySet(array $keySet) { $keys = $this->pluck('keys', $keySet, false); if ($keys) { - $keySet['keys'] = $this->formatListForApi($keys); + $keySet['keys'] = []; + + foreach ($keys as $key) { + $keySet['keys'][] = $this->formatListForApi([$key]); + } } if (isset($keySet['ranges'])) { @@ -649,7 +663,17 @@ private function createTransactionSelector(array &$args) { $selector = new TransactionSelector; if (isset($args['transaction'])) { - $selector = $selector->deserialize($this->pluck('transaction', $args), $this->codec); + $transaction = $this->pluck('transaction', $args); + + if (isset($transaction['singleUse'])) { + $transaction['singleUse'] = $this->formatTransactionOptions($transaction['singleUse']); + } + + if (isset($transaction['begin'])) { + $transaction['begin'] = $this->formatTransactionOptions($transaction['begin']); + } + + $selector = $selector->deserialize($transaction, $this->codec); } elseif (isset($args['transactionId'])) { $selector = $selector->deserialize(['id' => $this->pluck('transactionId', $args)], $this->codec); } @@ -677,4 +701,69 @@ private function instanceObject(array &$args, $required = false) 'labels' => $labels ]), $this->codec); } + + /** + * @param mixed $param + * @return Value + */ + private function fieldValue($param) + { + $field = new protobuf\Value; + $value = $this->formatValueForApi($param); + + switch (array_keys($value)[0]) { + case 'string_value': + $setter = 'setStringValue'; + break; + case 'number_value': + $setter = 'setNumberValue'; + break; + case 'bool_value': + $setter = 'setBoolValue'; + break; + case 'null_value': + $setter = 'setNullValue'; + break; + case 'struct_value': + $setter = 'setStructValue'; + break; + case 'list_value': + $setter = 'setListValue'; + $list = new protobuf\ListValue; + foreach ($param as $item) { + $list->addValues($this->fieldValue($item)); + } + + $value = $list; + + break; + } + + $value = is_array($value) ? current($value) : $value; + $field->$setter($value); + + return $field; + } + + /** + * @param array $transactionOptions + * @return array + */ + private function formatTransactionOptions(array $transactionOptions) + { + if (isset($transactionOptions['readOnly'])) { + $ro = $transactionOptions['readOnly']; + if (isset($ro['minReadTimestamp'])) { + $ro['minReadTimestamp'] = $this->formatTimestampForApi($ro['minReadTimestamp']); + } + + if (isset($ro['readTimestamp'])) { + $ro['readTimestamp'] = $this->formatTimestampForApi($ro['readTimestamp']); + } + + $transactionOptions['readOnly'] = $ro; + } + + return $transactionOptions; + } } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 9f24360819ab..d2c5c1340055 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -96,7 +96,7 @@ class Database use LROTrait; use TransactionConfigurationTrait; - const MAX_RETRIES = 3; + const MAX_RETRIES = 10; /** * @var ConnectionInterface @@ -634,7 +634,7 @@ public function transaction(array $options = []) * Configuration Options * * @type int $maxRetries The number of times to attempt to apply the - * operation before failing. **Defaults to ** `3`. + * operation before failing. **Defaults to ** `10`. * @type bool $singleUse If true, a Transaction ID will not be allocated * up front. Instead, the transaction will be considered * "single-use", and may be used for only a single operation. Note @@ -658,6 +658,10 @@ public function runTransaction(callable $operation, array $options = []) $attempt = 0; $startTransactionFn = function ($session, $options) use (&$attempt) { + if ($attempt > 0) { + $options['isRetry'] = true; + } + $transaction = $this->operation->transaction($session, $options); $attempt++; @@ -681,7 +685,9 @@ public function runTransaction(callable $operation, array $options = []) $res = call_user_func($operation, $transaction); - if ($transaction->state() === Transaction::STATE_ACTIVE) { + $active = $transaction->state() === Transaction::STATE_ACTIVE; + $singleUse = $transaction->type() === Transaction::TYPE_SINGLE_USE; + if ($active && !$singleUse) { $transaction->rollback($options); throw new \RuntimeException('Transactions must be rolled back or committed.'); } @@ -1091,7 +1097,10 @@ public function delete($table, KeySet $keySet, array $options = []) * `ValueMapper::TYPE_FLOAT64`, `ValueMapper::TYPE_TIMESTAMP`, * `ValueMapper::TYPE_DATE`, `ValueMapper::TYPE_STRING`, * `ValueMapper::TYPE_BYTES`, `ValueMapper::TYPE_ARRAY` and - * `ValueMapper::TYPE_STRUCT`. + * `ValueMapper::TYPE_STRUCT`. If the parameter type is an array, + * the type should be given as an array, where the first element + * is `ValueMapper::TYPE_ARRAY` and the second element is the + * array type, for instance `[ValueMapper::TYPE_ARRAY, ValueMapper::TYPE_INT64]`. * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected * read timestamp is included in the Transaction message that * describes the transaction. diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php index 3e6b00324467..759a779d6f94 100644 --- a/src/Spanner/KeyRange.php +++ b/src/Spanner/KeyRange.php @@ -87,27 +87,33 @@ class KeyRange * * @type string $startType Either "open" or "closed". Use constants * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for - * guaranteed correctness. + * guaranteed correctness. **Defaults to** `KeyRange::TYPE_OPEN`. * @type array $start The key with which to start the range. * @type string $endType Either "open" or "closed". Use constants * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for - * guaranteed correctness. + * guaranteed correctness. **Defaults to** `KeyRange::TYPE_OPEN`. * @type array $end The key with which to end the range. * } */ public function __construct(array $options = []) { $options += [ - 'startType' => null, + 'startType' => KeyRange::TYPE_OPEN, 'start' => null, - 'endType' => null, + 'endType' => KeyRange::TYPE_OPEN, 'end' => null ]; - $this->startType = $options['startType']; - $this->start = $options['start']; - $this->endType = $options['endType']; - $this->end = $options['end']; + $this->startType = $this->fromDefinition($options['startType'], 'start'); + $this->endType = $this->fromDefinition($options['endType'], 'end'); + + $this->start = ($options['start'] === null || is_array($options['start'])) + ? $options['start'] + : [$options['start']]; + + $this->end = ($options['end'] === null || is_array($options['end'])) + ? $options['end'] + : [$options['end']]; } /** @@ -165,14 +171,7 @@ public function start() */ public function setStart($type, array $start) { - if (!in_array($type, array_keys($this->definition))) { - throw new \InvalidArgumentException(sprintf( - 'Invalid KeyRange type. Allowed values are %s', - implode(', ', array_keys($this->definition)) - )); - } - - $rangeKey = $this->definition[$type]['start']; + $rangeKey = $this->fromDefinition($type, 'start'); $this->startType = $rangeKey; $this->start = $start; @@ -216,7 +215,7 @@ public function setEnd($type, array $end) )); } - $rangeKey = $this->definition[$type]['end']; + $rangeKey = $this->fromDefinition($type, 'end'); $this->endType = $rangeKey; $this->end = $end; @@ -257,4 +256,17 @@ public function keyRangeObject() $this->endType => $this->end ]; } + + private function fromDefinition($type, $startOrEnd) + { + if (!in_array($type, array_keys($this->definition))) { + throw new \InvalidArgumentException(sprintf( + 'Invalid KeyRange %s type. Allowed values are %s.', + $startOrEnd, + implode(', ', array_keys($this->definition)) + )); + } + + return $this->definition[$type][$startOrEnd]; + } } diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index bc4fde898770..54ce03b2ca50 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -168,7 +168,8 @@ public function execute(Session $session, $sql, array $options = []) ]; $parameters = $this->pluck('parameters', $options); - $options += $this->mapper->formatParamsForExecuteSql($parameters, $options['types']); + $types = $this->pluck('types', $options); + $options += $this->mapper->formatParamsForExecuteSql($parameters, $types); $context = $this->pluck('transactionContext', $options); @@ -242,13 +243,17 @@ public function read(Session $session, $table, KeySet $keySet, array $columns, a * up front. Instead, the transaction will be considered * "single-use", and may be used for only a single operation. * **Defaults to** `false`. + * @type bool $isRetry If true, the resulting transaction will indicate + * that it is the result of a retry operation. **Defaults to** + * `false`. * } * @return Transaction */ public function transaction(Session $session, array $options = []) { $options += [ - 'singleUse' => false + 'singleUse' => false, + 'isRetry' => false ]; if (!$options['singleUse']) { @@ -296,15 +301,20 @@ public function snapshot(Session $session, array $options = []) * * @param Session $session The session the transaction belongs to. * @param array $res [optional] The createTransaction response. + * @param array $options [optional] Options for the transaction object. * @return Transaction */ - public function createTransaction(Session $session, array $res = []) + public function createTransaction(Session $session, array $res = [], array $options = []) { $res += [ 'id' => null ]; - return new Transaction($this, $session, $res['id']); + $options['isRetry'] = isset($options['isRetry']) + ? $options['isRetry'] + : false; + + return new Transaction($this, $session, $res['id'], $options['isRetry']); } /** @@ -356,25 +366,18 @@ private function beginTransaction(Session $session, array $options = []) */ private function flattenKeySet(KeySet $keySet) { - $keyRanges = $keySet->ranges(); - if ($keyRanges) { - $ranges = []; - foreach ($keyRanges as $range) { - $types = $range->types(); - - $start = $range->start(); - $range->setStart($types['start'], $this->mapper->encodeValuesAsSimpleType($start)); + $keys = $keySet->keySetObject(); - $end = $range->end(); - $range->setEnd($types['end'], $this->mapper->encodeValuesAsSimpleType($end)); + if (!empty($keys['ranges'])) { + foreach ($keys['ranges'] as $index => $range) { + foreach ($range as $type => $rangeKeys) { + $range[$type] = $this->mapper->encodeValuesAsSimpleType($rangeKeys); + } - $ranges[] = $range; + $keys['ranges'][$index] = $range; } - - $keySet->setRanges($ranges); } - $keys = $keySet->keySetObject(); if (!empty($keys['keys'])) { $keys['keys'] = $this->mapper->encodeValuesAsSimpleType($keys['keys']); } diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 86c4ae32e448..7296c7efa35f 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -91,6 +91,19 @@ * @type array $parameters A key/value array of Query Parameters, where * the key is represented in the query string prefixed by a `@` * symbol. + * @type array $types A key/value array of Query Parameter types. + * Generally, Google Cloud PHP can infer types. Explicit type + * definitions are only necessary for null parameter values. + * Accepted values are defined as constants on + * {@see Google\Cloud\Spanner\ValueMapper}, and are as follows: + * `ValueMapper::TYPE_BOOL`, `ValueMapper::TYPE_INT64`, + * `ValueMapper::TYPE_FLOAT64`, `ValueMapper::TYPE_TIMESTAMP`, + * `ValueMapper::TYPE_DATE`, `ValueMapper::TYPE_STRING`, + * `ValueMapper::TYPE_BYTES`, `ValueMapper::TYPE_ARRAY` and + * `ValueMapper::TYPE_STRUCT`. If the parameter type is an array, + * the type should be given as an array, where the first element + * is `ValueMapper::TYPE_ARRAY` and the second element is the + * array type, for instance `[ValueMapper::TYPE_ARRAY, ValueMapper::TYPE_INT64]`. * } * @return Result * } @@ -146,6 +159,11 @@ class Transaction implements TransactionalReadInterface */ private $mutations = []; + /** + * @var bool + */ + private $isRetry = false; + /** * @param Operation $operation The Operation instance. * @param Session $session The session to use for spanner interactions. @@ -155,11 +173,13 @@ class Transaction implements TransactionalReadInterface public function __construct( Operation $operation, Session $session, - $transactionId = null + $transactionId = null, + $isRetry = false ) { $this->operation = $operation; $this->session = $session; $this->transactionId = $transactionId; + $this->isRetry = $isRetry; $this->type = $transactionId ? self::TYPE_PRE_ALLOCATED @@ -475,6 +495,29 @@ public function state() return $this->state; } + /** + * Check whether the current transaction is a retry transaction. + * + * When using {@see Google\Cloud\Spanner\Database::runTransaction()}, + * transactions are automatically retried when a conflict causes it to abort. + * In such cases, subsequent invocations of the transaction callable will + * provide a transaction where `$transaction->isRetry()` is true. This can + * be useful for debugging and understanding how code is working. + * + * Example: + * ``` + * if ($transaction->isRetry()) { + * echo 'This is a retry transaction!'; + * } + * ``` + * + * @return bool + */ + public function isRetry() + { + return $this->isRetry; + } + /** * Format, validate and enqueue mutations in the transaction. * diff --git a/src/Spanner/TransactionalReadTrait.php b/src/Spanner/TransactionalReadTrait.php index 5fcbf6042ef4..c220e3ed61ce 100644 --- a/src/Spanner/TransactionalReadTrait.php +++ b/src/Spanner/TransactionalReadTrait.php @@ -71,6 +71,19 @@ trait TransactionalReadTrait * @type array $parameters A key/value array of Query Parameters, where * the key is represented in the query string prefixed by a `@` * symbol. + * @type array $types A key/value array of Query Parameter types. + * Generally, Google Cloud PHP can infer types. Explicit type + * definitions are only necessary for null parameter values. + * Accepted values are defined as constants on + * {@see Google\Cloud\Spanner\ValueMapper}, and are as follows: + * `ValueMapper::TYPE_BOOL`, `ValueMapper::TYPE_INT64`, + * `ValueMapper::TYPE_FLOAT64`, `ValueMapper::TYPE_TIMESTAMP`, + * `ValueMapper::TYPE_DATE`, `ValueMapper::TYPE_STRING`, + * `ValueMapper::TYPE_BYTES`, `ValueMapper::TYPE_ARRAY` and + * `ValueMapper::TYPE_STRUCT`. If the parameter type is an array, + * the type should be given as an array, where the first element + * is `ValueMapper::TYPE_ARRAY` and the second element is the + * array type, for instance `[ValueMapper::TYPE_ARRAY, ValueMapper::TYPE_INT64]`. * } * @return Result */ diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index 8bd3507e90d4..21f8dc2bdfbe 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -959,7 +959,7 @@ public function streamingRead($session, $table, $columns, $keySet, $optionalArgs $mergedSettings, $this->descriptors['streamingRead'] ); - +// print_r($request->serialize(new PhpArray));exit; return $callable( $request, [], diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index 8118fe72df65..f6a2a238e53e 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -28,7 +28,7 @@ class ValueMapper { use ArrayTrait; - const NANO_REGEX = '/\.(\d{1,9})Z/'; + const NANO_REGEX = '/(?:\.(\d{1,9})Z)|(?:Z)/'; const TYPE_BOOL = TypeCode::TYPE_BOOL; const TYPE_INT64 = TypeCode::TYPE_INT64; @@ -90,6 +90,11 @@ public function formatParamsForExecuteSql(array $parameters, array $types = []) } $type = isset($types[$key]) ? $types[$key] : null; + $arrayType = null; + if (is_array($type)) { + $arrayType = $type[1]; + $type = $type[0]; + } if ($type !== null && !in_array($type, $this->allowedTypes)) { throw new \BadMethodCallException(sprintf( @@ -99,7 +104,15 @@ public function formatParamsForExecuteSql(array $parameters, array $types = []) )); } - list ($parameters[$key], $paramTypes[$key]) = $this->paramType($value, $type); + if ($arrayType !== null && !in_array($arrayType, $this->allowedTypes)) { + throw new \BadMethodCallException(sprintf( + 'Type %s given for parameter @%s is not valid.', + $type, + $key + )); + } + + list ($parameters[$key], $paramTypes[$key]) = $this->paramType($value, $type, $arrayType); } return [ @@ -166,7 +179,7 @@ public function createTimestampWithNanos($timestamp) preg_match(self::NANO_REGEX, $timestamp, $matches); $timestamp = preg_replace(self::NANO_REGEX, '.000000Z', $timestamp); - $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $timestamp); + $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, str_replace('..', '.', $timestamp)); return new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0); } @@ -254,31 +267,47 @@ private function decodeValue($value, array $type) * Create a spanner parameter type value object from a PHP value type. * * @param mixed $value The PHP value + * @param int $givenType + * @param int $arrayType * @return array The Value type */ - private function paramType($value, $givenType = null) + private function paramType($value, $givenType = null, $arrayType = null) { $phpType = gettype($value); switch ($phpType) { case 'boolean': - $type = $this->typeObject(self::TYPE_BOOL); + $type = $this->typeObject($givenType ?: self::TYPE_BOOL); break; case 'integer': $value = (string) $value; - $type = $this->typeObject(self::TYPE_INT64); + $type = $this->typeObject($givenType ?: self::TYPE_INT64); break; case 'double': - $type = $this->typeObject(self::TYPE_FLOAT64); + $type = $this->typeObject($givenType ?: self::TYPE_FLOAT64); + switch ($value) { + case INF: + $value = 'Infinity'; + break; + + case -INF: + $value = '-Infinity'; + break; + } + + if (!is_string($value) && is_nan($value)) { + $value = 'NaN'; + } + break; case 'string': - $type = $this->typeObject(self::TYPE_STRING); + $type = $this->typeObject($givenType ?: self::TYPE_STRING); break; case 'resource': - $type = $this->typeObject(self::TYPE_BYTES); + $type = $this->typeObject($givenType ?: self::TYPE_BYTES); $value = base64_encode(stream_get_contents($value)); break; @@ -287,7 +316,7 @@ private function paramType($value, $givenType = null) break; case 'array': - if ($this->isAssoc($value)) { + if (!empty($value) && $this->isAssoc($value)) { throw new \BadMethodCallException( 'Associative arrays are not supported. Did you mean to call a batch method?' ); @@ -303,13 +332,13 @@ private function paramType($value, $givenType = null) } } - if (count(array_unique($types)) !== 1) { + if (count(array_unique($types)) > 1) { throw new \BadMethodCallException('Array values may not be of mixed type'); } $type = $this->typeObject( self::TYPE_ARRAY, - $this->typeObject($types[0]), + $this->typeObject((isset($types[0])) ? $types[0] : null), 'arrayElementType' ); @@ -317,7 +346,15 @@ private function paramType($value, $givenType = null) break; case 'NULL': - $type = $this->typeObject($givenType); + if ($givenType === self::TYPE_ARRAY) { + $type = $this->typeObject( + $givenType, + $this->typeObject($arrayType), + 'arrayElementType' + ); + } else { + $type = $this->typeObject($givenType); + } break; default: diff --git a/tests/system/Spanner/ReadTest.php b/tests/system/Spanner/ReadTest.php new file mode 100644 index 000000000000..ed6eb1e025ce --- /dev/null +++ b/tests/system/Spanner/ReadTest.php @@ -0,0 +1,389 @@ +updateDdlBatch([ + 'CREATE TABLE '. self::READ_TABLE_NAME .' ( + id INT64 NOT NULL, + val STRING(MAX) NOT NULL, + ) PRIMARY KEY (id)', + 'CREATE TABLE '. self::RANGES_TABLE_NAME .' ( + id INT64 NOT NULL, + val STRING(MAX) NOT NULL, + ) PRIMARY KEY (id, val)' + ])->pollUntilComplete(); + + self::$dataset = self::generateDataset(20, true); + $db->insertBatch(self::RANGES_TABLE_NAME, self::$dataset); + } + + /** + * covers 12 + */ + public function testReadPoint() + { + $dataset = $this->generateDataset(); + + $db = self::$database; + $db->insertBatch(self::READ_TABLE_NAME, $dataset); + + $indexes = array_rand($dataset, 4); + $points = []; + $keys = []; + array_walk($indexes, function ($index) use ($dataset, &$points, &$keys) { + $points[] = $dataset[$index]; + $keys[] = $dataset[$index]['id']; + }); + + $keyset = new KeySet(['keys' => $keys]); + + $res = $db->read(self::READ_TABLE_NAME, $keyset, array_keys($dataset[0])); + $rows = $res->rows(); + foreach ($rows as $index => $row) { + $this->assertTrue(in_array($row, $dataset)); + $this->assertTrue(in_array($row, $points)); + } + } + + // public function rangeProvider() + // { + // return [ + // // single key, open-open + + // ]; + // } + + /** + * covers 8 + */ + public function testRangeReadSingleKeyOpen() + { + $db = self::$database; + + $range = new KeyRange([ + 'start' => self::$dataset[0], + 'end' => self::$dataset[10], + ]); + + $keyset = new KeySet(['ranges' => [$range]]); + + $res = $db->read(self::RANGES_TABLE_NAME, $keyset, array_keys(self::$dataset[0])); + $rows = iterator_to_array($res->rows()); + $this->assertFalse(in_array(self::$dataset[0], $rows)); + $this->assertFalse(in_array(self::$dataset[10], $rows)); + } + + /** + * covers 8 + */ + public function testRangeReadSingleKeyClosed() + { + $db = self::$database; + + $range = new KeyRange([ + 'start' => self::$dataset[0], + 'end' => self::$dataset[10], + 'startType' => KeyRange::TYPE_CLOSED, + 'endType' => KeyRange::TYPE_CLOSED, + ]); + + $keyset = new KeySet(['ranges' => [$range]]); + + $res = $db->read(self::RANGES_TABLE_NAME, $keyset, array_keys(self::$dataset[0])); + $rows = iterator_to_array($res->rows()); + $this->assertTrue(in_array(self::$dataset[0], $rows)); + $this->assertTrue(in_array(self::$dataset[10], $rows)); + } + + /** + * covers 8 + */ + public function testRangeReadSingleKeyOpenClosed() + { + $db = self::$database; + + $range = new KeyRange([ + 'start' => self::$dataset[0], + 'end' => self::$dataset[10], + 'endType' => KeyRange::TYPE_CLOSED + ]); + + $keyset = new KeySet(['ranges' => [$range]]); + + $res = $db->read(self::RANGES_TABLE_NAME, $keyset, array_keys(self::$dataset[0])); + $rows = iterator_to_array($res->rows()); + $this->assertFalse(in_array(self::$dataset[0], $rows)); + $this->assertTrue(in_array(self::$dataset[10], $rows)); + } + + /** + * covers 8 + */ + public function testRangeReadSingleKeyClosedOpen() + { + $db = self::$database; + + $range = new KeyRange([ + 'start' => self::$dataset[0], + 'startType' => KeyRange::TYPE_CLOSED, + 'end' => self::$dataset[10], + ]); + + $keyset = new KeySet(['ranges' => [$range]]); + + $res = $db->read(self::RANGES_TABLE_NAME, $keyset, array_keys(self::$dataset[0])); + $rows = iterator_to_array($res->rows()); + $this->assertTrue(in_array(self::$dataset[0], $rows)); + $this->assertFalse(in_array(self::$dataset[10], $rows)); + } + + /** + * covers 8 + */ + public function testRangeReadPartialKeyOpen() + { + $db = self::$database; + + $range = new KeyRange([ + 'start' => [self::$dataset[0]['id']], + 'end' => [self::$dataset[10]['id']], + ]); + + $keyset = new KeySet(['ranges' => [$range]]); + + $res = $db->read(self::RANGES_TABLE_NAME, $keyset, array_keys(self::$dataset[0])); + $rows = iterator_to_array($res->rows()); + $this->assertFalse(in_array(self::$dataset[0], $rows)); + $this->assertFalse(in_array(self::$dataset[10], $rows)); + } + + /** + * covers 8 + */ + public function testRangeReadPartialKeyClosed() + { + $db = self::$database; + + $range = new KeyRange([ + 'start' => [self::$dataset[0]['id']], + 'end' => [self::$dataset[10]['id']], + 'startType' => KeyRange::TYPE_CLOSED, + 'endType' => KeyRange::TYPE_CLOSED, + ]); + + $keyset = new KeySet(['ranges' => [$range]]); + + $res = $db->read(self::RANGES_TABLE_NAME, $keyset, array_keys(self::$dataset[0])); + $rows = iterator_to_array($res->rows()); + $this->assertTrue(in_array(self::$dataset[0], $rows)); + $this->assertTrue(in_array(self::$dataset[10], $rows)); + } + + /** + * covers 21 + */ + public function testQueryReturnsArrayStruct() + { + $db = self::$database; + + $res = $db->execute('SELECT ARRAY(SELECT STRUCT(1, 2))'); + $row = $res->rows()->current(); + $this->assertEquals($row[0][0], [1,2]); + } + + /** + * covers 23 + */ + public function testBindBoolParameter() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => true + ] + ]); + + $row = $res->rows()->current(); + $this->assertTrue($row['foo']); + } + + /** + * covers 25 + */ + public function testBindInt64Parameter() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => 1337 + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals(1337, $row['foo']); + } + + /** + * covers 25 + */ + public function testBindInt64ParameterWithInt64Class() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => new Int64('1337') + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals(1337, $row['foo']); + } + + /** + * covers 26 + */ + public function testBindNullIntParameter() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => ValueMapper::TYPE_INT64 + ] + ]); + + $row = $res->rows()->current(); + $this->assertNull($row['foo']); + } + + /** + * covers 27 + */ + public function testBindFloat64Parameter() + { + $db = self::$database; + + $pi = 3.1415; + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $pi + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals($pi, $row['foo']); + } + + /** + * covers 29 + */ + public function testBindStringParameter() + { + $db = self::$database; + + $str = 'hello world'; + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $str + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals($str, $row['foo']); + } + + /** + * covers 31 + */ + public function testBindBytesParameter() + { + $db = self::$database; + + $str = 'hello world'; + $bytes = new Bytes($str); + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $bytes + ] + ]); + + $row = $res->rows()->current(); + $this->assertInstanceOf(Bytes::class, $row['foo']); + $this->assertEquals($str, base64_decode($bytes->formatAsString())); + } + + /** + * covers 40 + */ + public function testBindInt64ArrayParameter() + { + $db = self::$database; + + $arr = [5,4,3,2,1]; + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $arr + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals($arr, $row['foo']); + } + + private static function generateDataset($count = 20, $ordered = false) + { + $dataset = []; + for ($i = 0; $i < $count; $i++) { + $id = ($ordered) ? $i : self::randId(); + $dataset[] = [ + 'id' => $id, + 'val' => uniqid(self::TESTING_PREFIX) + ]; + } + + return $dataset; + } +} diff --git a/tests/system/Spanner/SnapshotTest.php b/tests/system/Spanner/SnapshotTest.php index 06becac07d3b..e083a8a9e05c 100644 --- a/tests/system/Spanner/SnapshotTest.php +++ b/tests/system/Spanner/SnapshotTest.php @@ -18,39 +18,200 @@ namespace Google\Cloud\Tests\System\Spanner; use Google\Cloud\Spanner\Date; +use Google\Cloud\Spanner\Duration; +use Google\Cloud\Spanner\Timestamp; /** * @group spanner + * @group spanner-snapshot */ class SnapshotTest extends SpannerTestCase { - private $id; + const TABLE_NAME = 'Snapshots'; - public function setUp() + private static $tableName; + + public static function setupBeforeClass() { - $this->id = rand(1,99999); + parent::setUpBeforeClass(); + + self::$tableName = uniqid(self::TABLE_NAME); + + self::$database->updateDdl( + 'CREATE TABLE '. self::$tableName .' ( + id INT64 NOT NULL, + number INT64 NOT NULL + ) PRIMARY KEY (id)' + )->pollUntilComplete(); } - public function testSnapshot() + /** + * covers 63 + * covers 68 + */ + public function testSnapshotStrongRead() { $db = self::$database; - $db->insert('Users', [ - 'id' => $this->id, - 'name' => 'John', - 'birthday' => new Date(new \DateTime('1990-01-01')) + $id = $this->randId(); + $row = [ + 'id' => $id, + 'number' => 1 + ]; + + $db->insert(self::$tableName, $row); + + $snapshot = $db->snapshot(['strong' => true, 'returnReadTimestamp' => true]); + + $newRow = $row; + $newRow['number'] = 2; + $db->replace(self::$tableName, $newRow); + + $res = $this->getRow($snapshot, $id); + $this->assertEquals($res, $row); + $this->assertInstanceOf(Timestamp::class, $snapshot->readTimestamp()); + } + + /** + * covers 64 + * covers 69 + */ + public function testSnapshotExactTimestampRead() + { + $db = self::$database; + + $id = $this->randId(); + $row = [ + 'id' => $id, + 'number' => 1 + ]; + + $db->insert(self::$tableName, $row); + sleep(1); + $ts = new Timestamp(new \DateTimeImmutable); + sleep(1); + + $newRow = $row; + $newRow['number'] = 2; + $db->replace(self::$tableName, $newRow); + + $snapshot = $db->snapshot([ + 'readTimestamp' => $ts, + 'returnReadTimestamp' => true + ]); + + $this->assertEquals($ts->get()->format('U'), $snapshot->readTimestamp()->get()->format('U')); + + $res = $this->getRow($snapshot, $id); + $this->assertEquals($row, $res); + } + + /** + * covers 65 + */ + public function testSnapshotMinReadTimestamp() + { + $db = self::$database; + + $id = $this->randId(); + $row = [ + 'id' => $id, + 'number' => 1 + ]; + + $db->insert(self::$tableName, $row); + sleep(1); + $ts = new Timestamp(new \DateTimeImmutable); + sleep(1); + + $newRow = $row; + $newRow['number'] = 2; + $db->replace(self::$tableName, $newRow); + + $snapshot = $db->snapshot([ + 'minReadTimestamp' => $ts, + 'singleUse' => true + ]); + + $res = $this->getRow($snapshot, $id); + $this->assertEquals($res, $newRow); + } + + /** + * covers 66 + * covers 70 + */ + public function testSnapshotExactStaleness() + { + $db = self::$database; + + $id = $this->randId(); + $row = [ + 'id' => $id, + 'number' => 1 + ]; + + $db->insert(self::$tableName, $row); + sleep(1); + $ts = new Timestamp(new \DateTimeImmutable); + sleep(1); + + $newRow = $row; + $newRow['number'] = 2; + $db->replace(self::$tableName, $newRow); + + $duration = new Duration(1); + + $snapshot = $db->snapshot([ + 'exactStaleness' => $duration, + 'returnReadTimestamp' => true + ]); + + $this->assertEquals($ts->get()->format('U'), $snapshot->readTimestamp()->get()->format('U')); + + $res = $this->getRow($snapshot, $id); + $this->assertEquals($row, $res); + } + + /** + * covers 67 + */ + public function testSnapshotMaxStaleness() + { + $db = self::$database; + + $id = $this->randId(); + $row = [ + 'id' => $id, + 'number' => 1 + ]; + + $db->insert(self::$tableName, $row); + sleep(1); + $ts = new Timestamp(new \DateTimeImmutable); + sleep(1); + + $newRow = $row; + $newRow['number'] = 2; + $db->replace(self::$tableName, $newRow); + + $duration = new Duration(1); + + $snapshot = $db->snapshot([ + 'maxStaleness' => $duration, + 'returnReadTimestamp' => true, + 'singleUse' => true ]); - $snapshot = $db->snapshot(); - $row = $this->getRow($snapshot); - $this->assertEquals('John', $row['name']); + $res = $this->getRow($snapshot, $id); + $this->assertEquals($res, $newRow); } - private function getRow($client) + private function getRow($client, $id) { - $result = $client->execute('SELECT * FROM Users WHERE id=@id', [ + $result = $client->execute('SELECT * FROM '. self::$tableName .' WHERE id=@id', [ 'parameters' => [ - 'id' => $this->id + 'id' => $id ] ]); diff --git a/tests/system/Spanner/SpannerTestCase.php b/tests/system/Spanner/SpannerTestCase.php index 61c6f406182d..c96c597d02ad 100644 --- a/tests/system/Spanner/SpannerTestCase.php +++ b/tests/system/Spanner/SpannerTestCase.php @@ -34,6 +34,7 @@ class SpannerTestCase extends \PHPUnit_Framework_TestCase protected static $client; protected static $instance; protected static $database; + protected static $database2; protected static $deletionQueue = []; private static $hasSetUp = false; @@ -73,6 +74,7 @@ public static function setUpBeforeClass() )->pollUntilComplete(); self::$database = $db; + self::$database2 = self::$client->connect(self::INSTANCE_NAME, $dbName); } public static function tearDownFixtures() @@ -83,4 +85,9 @@ public static function tearDownFixtures() $backoff->execute($item); } } + + public static function randId() + { + return rand(1,9999999); + } } diff --git a/tests/system/Spanner/TransactionTest.php b/tests/system/Spanner/TransactionTest.php index b9494cb837e9..cd8223678a02 100644 --- a/tests/system/Spanner/TransactionTest.php +++ b/tests/system/Spanner/TransactionTest.php @@ -23,15 +23,22 @@ /** * @group spanner + * @group spanner-transactions */ class TransactionTest extends SpannerTestCase { + const TABLE_NAME = 'Transactions'; + private static $row = []; + private static $tableName; + public static function setUpBeforeClass() { parent::setUpBeforeClass(); + self::$tableName = uniqid(self::TABLE_NAME); + self::$row = [ 'id' => rand(1000,9999), 'name' => uniqid(self::TESTING_PREFIX), @@ -39,6 +46,13 @@ public static function setUpBeforeClass() ]; self::$database->insert(self::TEST_TABLE_NAME, self::$row); + + self::$database->updateDdl( + 'CREATE TABLE '. self::$tableName .' ( + id INT64 NOT NULL, + number INT64 NOT NULL + ) PRIMARY KEY (id)' + )->pollUntilComplete(); } public function testRunTransaction() @@ -61,53 +75,198 @@ public function testRunTransaction() }); } - public function testStrongRead() + /** + * covers 73 + */ + public function testConcurrentTransactionsIncrementValueWithRead() { $db = self::$database; + $db2 = self::$database2; - $snapshot = $db->snapshot([ - 'strong' => true, - 'returnReadTimestamp' => true + $id = $this->randId(); + $db->insert(self::$tableName, [ + 'id' => $id, + 'number' => 0 ]); - list($keySet, $cols) = $this->readArgs(); - $res = $snapshot->read(self::TEST_TABLE_NAME, $keySet, $cols); + $keyset = new KeySet(['keys' => [$id]]); + $columns = ['id','number']; - $row = $res->rows()->current(); + $iteration = 0; + $db->runTransaction(function ($transaction) use ($db2, &$iteration, $keyset, $columns) { + $row = $transaction->read(self::$tableName, $keyset, $columns)->rows()->current(); - $this->assertEquals(self::$row, $row); - $this->assertInstanceOf(Timestamp::class, $snapshot->readTimestamp()); + if ($iteration === 0) { + $db2->runTransaction(function ($t2) use ($keyset, $columns) { + $row = $t2->read(self::$tableName, $keyset, $columns)->rows()->current(); + + $row['number'] = $row['number']+1; + + $t2->update(self::$tableName, $row); + $t2->commit(); + }); + } + + $row['number'] = $row['number']+1; + $iteration++; + + $transaction->update(self::$tableName, $row); + $transaction->commit(); + }); + + $row = $db->execute('SELECT * FROM '. self::$tableName .' WHERE id = @id', [ + 'parameters' => [ + 'id' => $id + ] + ])->rows()->current(); + + $this->assertEquals(2, $row['number']); + } + + /** + * covers 75 + */ + public function testTransactionNoCommit() + { + $db = self::$database; + + $ex = false; + try { + $db->runTransaction(function ($t) { + $t->execute('SELECT * FROM '. self::$tableName); + }); + } catch (\RuntimeException $e) { + $this->assertEquals('Transactions must be rolled back or committed.', $e->getMessage()); + $ex = true; + } + + $this->assertTrue($ex); } - public function testExactTimestampRead() + /** + * covers 76 + */ + public function testAbortedErrorCausesRetry() { $db = self::$database; + $db2 = self::$database2; + + $args = [ + 'id' => $this->randId(), + 'it' => 0, + 'pre' => null, + 'edit' => null, + 'post' => null + ]; + + $db->insert(self::$tableName, [ + 'id' => $args['id'], + 'number' => 0 + ]); + + $db->runTransaction(function ($t) use ($db2, &$args) { + if ($args['it'] === 0) { + $row = $t->execute('SELECT * FROM '. self::$tableName .' WHERE id = @id', [ + 'parameters' => ['id' => $args['id']] + ])->rows()->current(); + + $args['pre'] = $row['number']; - $ts = new Timestamp(new \DateTimeImmutable); + $db2->runTransaction(function ($t2) use (&$args) { + $row = $t2->execute('SELECT * FROM '. self::$tableName .' WHERE id = @id', [ + 'parameters' => ['id' => $args['id']] + ])->rows()->current(); - $row = $db->execute('SELECT * FROM '. self::TEST_TABLE_NAME .' WHERE id = @id', [ - 'parameters' => ['id' => self::$row['id']] + $args['edit'] = $row['number']+1; + $row['number'] = $args['edit']; + $t2->replace(self::$tableName, $row); + $t2->commit(); + }); + } + + $args['it']++; + + $row = $t->execute('SELECT * FROM '. self::$tableName .' WHERE id = @id', [ + 'parameters' => ['id' => $args['id']] + ])->rows()->current(); + + $args['post'] = $row['number']; + $this->assertEquals($row['number'], $args['post']); + $this->assertEquals($args['pre']+1, $row['number']); + + $t->rollback(); + }); + } + + /** + * covers 74 + */ + public function testConcurrentTransactionsIncrementValueWithExecute() + { + $db = self::$database; + $db2 = self::$database2; + + $id = $this->randId(); + $db->insert(self::$tableName, [ + 'id' => $id, + 'number' => 0 + ]); + + $iteration = 0; + $db->runTransaction(function ($transaction) use ($db2, $id, &$iteration) { + $row = $transaction->execute('SELECT * FROM '. self::$tableName .' WHERE id = @id', [ + 'parameters' => [ + 'id' => $id + ] + ])->rows()->current(); + + if ($iteration === 0) { + $db2->runTransaction(function ($t2) use ($id) { + $row = $t2->execute('SELECT * FROM '. self::$tableName .' WHERE id = @id', [ + 'parameters' => [ + 'id' => $id + ] + ])->rows()->current(); + + $row['number'] = $row['number']+1; + + $t2->update(self::$tableName, $row); + $t2->commit(); + }); + } + + $row['number'] = $row['number']+1; + $iteration++; + + $transaction->update(self::$tableName, $row); + $transaction->commit(); + }); + + $row = $db->execute('SELECT * FROM '. self::$tableName .' WHERE id = @id', [ + 'parameters' => [ + 'id' => $id + ] ])->rows()->current(); - $row['name'] = uniqid(self::TESTING_PREFIX); - $db->update(self::TEST_TABLE_NAME, $row); - sleep(10); + $this->assertEquals(2, $row['number']); + } + + public function testStrongRead() + { + $db = self::$database; $snapshot = $db->snapshot([ - 'returnReadTimestamp' => true, - 'readTimestamp' => $ts + 'strong' => true, + 'returnReadTimestamp' => true ]); list($keySet, $cols) = $this->readArgs(); + $res = $snapshot->read(self::TEST_TABLE_NAME, $keySet, $cols); - $res = $snapshot->read(self::TEST_TABLE_NAME, $keySet, $cols)->rows(); - $row = $res->current(); - - $this->assertEquals($ts->get(), $snapshot->readTimestamp()->get()); - $this->assertEquals($row, self::$row); + $row = $res->rows()->current(); - // Reset to previous state. - $db->update(self::TEST_TABLE_NAME, self::$row); + $this->assertEquals(self::$row, $row); + $this->assertInstanceOf(Timestamp::class, $snapshot->readTimestamp()); } private function readArgs() diff --git a/tests/system/Spanner/WriteTest.php b/tests/system/Spanner/WriteTest.php new file mode 100644 index 000000000000..738f536b0bf2 --- /dev/null +++ b/tests/system/Spanner/WriteTest.php @@ -0,0 +1,375 @@ +updateDdl( + 'CREATE TABLE '. self::TABLE_NAME .' ( + id INT64 NOT NULL, + arrayField ARRAY, + arrayBoolField ARRAY, + arrayFloatField ARRAY, + arrayStringField ARRAY, + arrayBytesField ARRAY, + arrayTimestampField ARRAY, + arrayDateField ARRAY, + boolField BOOL, + bytesField BYTES(MAX), + dateField DATE, + floatField FLOAT64, + intField INT64, + stringField STRING(MAX), + timestampField TIMESTAMP + ) PRIMARY KEY (id)' + )->pollUntilComplete(); + } + + public function fieldValueProvider() + { + return [ + [$this->randId(), 'boolField', false], + [$this->randId(), 'boolField', true], + [$this->randId(), 'arrayField', [1,2,3,4,5]], + [$this->randId(), 'dateField', new Date(new \DateTime('1981-01-20'))], + [$this->randId(), 'floatField', 3.1415], + [$this->randId(), 'floatField', INF], + [$this->randId(), 'floatField', -INF], + [$this->randId(), 'intField', 787878787], + [$this->randId(), 'stringField', 'foo bar'], + [$this->randId(), 'timestampField', new Timestamp(new \DateTime)] + ]; + } + + /** + * @dataProvider fieldValueProvider + * covers 78 + * covers 80 + * covers 82 + * covers 84 + * covers 85 + * covers 90 + * covers 92 + */ + public function testWriteAndReadBackValue($id, $field, $value) + { + $db = self::$database; + + $db->insert(self::TABLE_NAME, [ + 'id' => $id, + $field => $value + ]); + + // test result from read + $keyset = new KeySet(['keys' => [$id]]); + $read = $db->read(self::TABLE_NAME, $keyset, [$field]); + $row = $read->rows()->current(); + $this->assertEquals($value, $row[$field]); + + // test result from executeSql + $exec = $db->execute(sprintf('SELECT %s FROM %s WHERE id = @id', $field, self::TABLE_NAME), [ + 'parameters' => [ + 'id' => $id + ] + ]); + + $row = $exec->rows()->current(); + $this->assertEquals($value, $row[$field]); + } + + /** + * covers 87 + */ + public function testWriteAndReadBackBytes() + { + $id = $this->randId(); + $field = 'bytesField'; + $value = new Bytes('hello world'); + + $db = self::$database; + + $db->insert(self::TABLE_NAME, [ + 'id' => $id, + $field => $value + ]); + + $keyset = new KeySet(['keys' => [$id]]); + $read = $db->read(self::TABLE_NAME, $keyset, [$field]); + $row = $read->rows()->current(); + + $this->assertEquals($value->formatAsString(), $row[$field]->formatAsString()); + + $exec = $db->execute(sprintf('SELECT %s FROM %s WHERE id = @id', $field, self::TABLE_NAME), [ + 'parameters' => [ + 'id' => $id + ] + ]); + + $row = $exec->rows()->current(); + $this->assertEquals($value->formatAsString(), $row[$field]->formatAsString()); + } + + /** + * covers 84 + */ + public function testWriteAndReadBackNaN() + { + $id = $this->randId(); + $field = 'floatField'; + $value = NAN; + + $db = self::$database; + + $db->insert(self::TABLE_NAME, [ + 'id' => $id, + $field => $value + ]); + + $keyset = new KeySet(['keys' => [$id]]); + $read = $db->read(self::TABLE_NAME, $keyset, [$field]); + $row = $read->rows()->current(); + + $this->assertTrue(is_nan($row[$field])); + + $exec = $db->execute(sprintf('SELECT %s FROM %s WHERE id = @id', $field, self::TABLE_NAME), [ + 'parameters' => [ + 'id' => $id + ] + ]); + + $row = $exec->rows()->current(); + $this->assertTrue(is_nan($row[$field])); + } + + public function nullFieldValueProvider() + { + $provider = $this->fieldValueProvider(); + $provider[] = [$this->randId(), 'bytesField']; + + return $provider; + } + + /** + * @dataProvider nullFieldValueProvider + * covers 79 + * covers 81 + * covers 83 + * covers 86 + * covers 89 + * covers 91 + * covers 93 + */ + public function testWriteAndReadBackNullValue($id, $field) + { + $db = self::$database; + + $db->insert(self::TABLE_NAME, [ + 'id' => $id, + $field => null + ]); + + // test result from read + $keyset = new KeySet(['keys' => [$id]]); + $read = $db->read(self::TABLE_NAME, $keyset, [$field]); + $row = $read->rows()->current(); + $this->assertNull($row[$field]); + + // test result from executeSql + $exec = $db->execute(sprintf('SELECT %s FROM %s WHERE id = @id', $field, self::TABLE_NAME), [ + 'parameters' => [ + 'id' => $id + ] + ]); + + $row = $exec->rows()->current(); + $this->assertNull($row[$field]); + } + + public function arrayFieldValueProvider() + { + return [ + [$this->randId(), 'arrayField', []], + [$this->randId(), 'arrayField', [1,2,null,4,5]], + [$this->randId(), 'arrayField', null], + [$this->randId(), 'arrayBoolField', [true,false]], + [$this->randId(), 'arrayBoolField', []], + [$this->randId(), 'arrayBoolField', [true, false, null, false]], + [$this->randId(), 'arrayBoolField', null], + [$this->randId(), 'arrayFloatField', [1.1, 1.2, 1.3]], + [$this->randId(), 'arrayFloatField', []], + [$this->randId(), 'arrayFloatField', [1.1, null, 1.3]], + [$this->randId(), 'arrayFloatField', null], + [$this->randId(), 'arrayStringField', ['foo','bar','baz']], + [$this->randId(), 'arrayStringField', []], + [$this->randId(), 'arrayStringField', ['foo',null,'baz']], + [$this->randId(), 'arrayStringField', null], + [$this->randId(), 'arrayBytesField', []], + [$this->randId(), 'arrayBytesField', null], + [$this->randId(), 'arrayTimestampField', []], + [$this->randId(), 'arrayTimestampField', null], + [$this->randId(), 'arrayDateField', []], + [$this->randId(), 'arrayDateField', null], + ]; + } + + /** + * @dataProvider arrayFieldValueProvider + * covers 94 + * covers 95 + * covers 96 + * covers 97 + * covers 98 + * covers 99 + * covers 100 + * covers 101 + * covers 102 + * covers 103 + * covers 104 + * covers 105 + * covers 106 + * covers 107 + * covers 108 + * covers 109 + * covers 110 + * covers 111 + * covers 112 + * covers 113 + * covers 114 + */ + public function testWriteAndReadBackFancyArrayValue($id, $field, $value) + { + $db = self::$database; + + $db->insert(self::TABLE_NAME, [ + 'id' => $id, + $field => $value + ]); + + // test result from read + $keyset = new KeySet(['keys' => [$id]]); + $read = $db->read(self::TABLE_NAME, $keyset, [$field]); + $row = $read->rows()->current(); + + $this->assertEquals($value, $row[$field]); + + // test result from executeSql + $exec = $db->execute(sprintf('SELECT %s FROM %s WHERE id = @id', $field, self::TABLE_NAME), [ + 'parameters' => [ + 'id' => $id + ] + ]); + + $row = $exec->rows()->current(); + + if ($value instanceof Bytes) { + $this->assertEquals($value->formatAsString(), $row[$field]->formatAsString()); + } else { + $this->assertEquals($value, $row[$field]); + } + } + + public function arrayFieldComplexValueProvider() + { + return [ + [$this->randId(), 'arrayBytesField', [new Bytes('foo'),null,new Bytes('baz')]], + [$this->randId(), 'arrayTimestampField', [new Timestamp(new \DateTime),null,new Timestamp(new \DateTime)]], + [$this->randId(), 'arrayDateField', [new Date(new \DateTime),null,new Date(new \DateTime)]], + ]; + } + + /** + * @dataProvider arrayFieldComplexValueProvider + */ + public function testWriteAndReadBackFancyArrayComplexValue($id, $field, $value) + { + $db = self::$database; + + $db->insert(self::TABLE_NAME, [ + 'id' => $id, + $field => $value + ]); + + // test result from read + $keyset = new KeySet(['keys' => [$id]]); + $read = $db->read(self::TABLE_NAME, $keyset, [$field]); + + // test result from executeSql + $exec = $db->execute(sprintf('SELECT %s FROM %s WHERE id = @id', $field, self::TABLE_NAME), [ + 'parameters' => [ + 'id' => $id + ] + ]); + + $row1 = $read->rows()->current(); + $row2 = $exec->rows()->current(); + + foreach ($row2[$field] as $item) { + if (is_null($item)) continue; + + $this->assertInstanceOf(get_class($value[0]), $item); + } + } + + /** + * @expectedException Google\Cloud\Core\Exception\NotFoundException + */ + public function testWriteToNonExistentTableFails() + { + $db = self::$database; + + $db->insert(uniqid(self::TESTING_PREFIX), ['foo' => 'bar']); + } + + /** + * @expectedException Google\Cloud\Core\Exception\NotFoundException + */ + public function testWriteToNonExistentColumnFails() + { + $db = self::$database; + + $db->insert(self::TABLE_NAME, [uniqid(self::TESTING_PREFIX) => 'bar']); + } + + /** + * @expectedException Google\Cloud\Core\Exception\FailedPreconditionException + */ + public function testWriteIncorrectTypeToColumn() + { + $db = self::$database; + + $db->insert(self::TABLE_NAME, [ + 'id' => $this->randId(), + 'boolField' => 'bar' + ]); + } +} diff --git a/tests/unit/Spanner/Connection/GrpcTest.php b/tests/unit/Spanner/Connection/GrpcTest.php index 9b486e493efb..e158a7ebd702 100644 --- a/tests/unit/Spanner/Connection/GrpcTest.php +++ b/tests/unit/Spanner/Connection/GrpcTest.php @@ -154,21 +154,7 @@ public function methodProvider() 'columns' => ['foo'], 'values' => ['bar'] ] - ], - // [ - // 'delete' => [ - // 'table' => $tableName, - // 'keySet' => [ - // 'keys' => ['foo','bar'], - // 'ranges' => [ - // [ - // 'startOpen' => ['foo'], - // 'endClosed' => ['bar'] - // ] - // ] - // ] - // ] - // ] + ] ]; $insertMutationsArr = []; @@ -181,14 +167,6 @@ public function methodProvider() $mutation->setInsert($operation); $insertMutationsArr[] = $mutation; - // $delete = $mutations[1]['delete']; - // $delete['keySet']['keys'] = $this->formatListForApi($delete['keySet']['keys']); - // $operation = (new Mutation\Delete) - // ->deserialize($delete, $codec); - // $mutation = new Mutation; - // $mutation->setDelete($operation); - // $mutationsArr[] = $mutation; - return [ [ 'listInstanceConfigs', @@ -334,11 +312,11 @@ public function methodProvider() [$sessionName, $readOnlyTransactionOptions, []] ], // test insert - [ - 'commit', - ['session' => $sessionName, 'mutations' => $insertMutations], - [$sessionName, $insertMutationsArr, []] - ], + // [ + // 'commit', + // ['session' => $sessionName, 'mutations' => $insertMutations], + // [$sessionName, $insertMutationsArr, []] + // ], // test single-use transaction [ 'commit', diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php index f6dc5692973c..6ea000dd4063 100644 --- a/tests/unit/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/DatabaseTest.php @@ -369,7 +369,15 @@ public function testRunTransactionRetry() $this->refreshOperation(); - $this->database->runTransaction(function($t){$t->commit();}); + $this->database->runTransaction(function($t) use ($it) { + if ($it > 0) { + $this->assertTrue($t->isRetry()); + } else { + $this->assertFalse($t->isRetry()); + } + + $t->commit(); + }); } /** @@ -395,7 +403,7 @@ public function testRunTransactionAborted() ->shouldBeCalled() ->will(function() use (&$it, $abort) { $it++; - if ($it <= 8) { + if ($it <= Database::MAX_RETRIES+1) { throw $abort; } diff --git a/tests/unit/Spanner/KeyRangeTest.php b/tests/unit/Spanner/KeyRangeTest.php index 8d378ab3df4b..0b8703083455 100644 --- a/tests/unit/Spanner/KeyRangeTest.php +++ b/tests/unit/Spanner/KeyRangeTest.php @@ -58,7 +58,7 @@ public function testGetters() $this->assertEquals(['foo'], $range->start()); $this->assertEquals(['bar'], $range->end()); - $this->assertEquals(['start' => KeyRange::TYPE_CLOSED, 'end' => KeyRange::TYPE_OPEN], $range->types()); + $this->assertEquals(['start' => 'startClosed', 'end' => 'endOpen'], $range->types()); } public function testSetStart() diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php index cafceb6fc8fa..eabceddf61bb 100644 --- a/tests/unit/Spanner/TransactionTest.php +++ b/tests/unit/Spanner/TransactionTest.php @@ -282,6 +282,25 @@ public function testInvalidReadContext() $this->singleUseTransaction->execute('foo'); } + public function testIsRetryFalse() + { + $this->assertFalse($this->transaction->isRetry()); + } + + public function testIsRetryTrue() + { + $args = [ + $this->operation, + $this->session, + self::TRANSACTION, + true + ]; + + $transaction = \Google\Cloud\Dev\stub(Transaction::class, $args); + + $this->assertTrue($transaction->isRetry()); + } + // ******* // Helpers diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php index d5d83eabf9ea..614860f9dde4 100644 --- a/tests/unit/Spanner/ValueMapperTest.php +++ b/tests/unit/Spanner/ValueMapperTest.php @@ -163,8 +163,7 @@ public function testEncodeValuesAsSimpleType() $this->assertEquals((string) $vals['int'], $res[1]); $this->assertEquals((string) $vals['int'], $res[2]); $this->assertEquals($vals['float'], $res[3]); - $this->assertTrue(is_nan($res[4])); - $this->assertEquals(INF, $res[5]); + $this->assertEquals('Infinity', $res[5]); $this->assertEquals($dt->format(Timestamp::FORMAT), $res[6]); $this->assertEquals($dt->format(Date::FORMAT), $res[7]); $this->assertEquals($vals['string'], $res[8]); From 927b91988b1a75fbf7d19c3ee3c206035133065f Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Fri, 12 May 2017 14:16:26 -0400 Subject: [PATCH 10/46] allow user determined row formatting doc fix address code review move symfony/lock from require to suggest --- composer.json | 11 +- docs/contents/cloud-spanner.json | 3 + src/Spanner/Database.php | 14 +- src/Spanner/Result.php | 117 +++++-- src/Spanner/Session/CacheSessionPool.php | 329 +++++++++++++++--- src/Spanner/ValueMapper.php | 57 ++- tests/snippets/Spanner/ResultTest.php | 20 ++ tests/unit/Spanner/ResultTest.php | 155 ++++++++- tests/unit/Spanner/ResultTestTrait.php | 39 ++- .../Spanner/Session/CacheSessionPoolTest.php | 227 ++++++++++-- tests/unit/Spanner/ValueMapperTest.php | 99 +++++- .../streaming-read-acceptance-test.json | 76 ++++ 12 files changed, 997 insertions(+), 150 deletions(-) diff --git a/composer.json b/composer.json index aaff7d30aaa7..4d56dddc8efe 100644 --- a/composer.json +++ b/composer.json @@ -40,14 +40,13 @@ } ], "require": { - "php": ">=5.5.9", + "php": ">=5.5", "rize/uri-template": "~0.3", "google/auth": "^0.11", "guzzlehttp/guzzle": "^5.3|^6.0", "guzzlehttp/psr7": "^1.2", "monolog/monolog": "~1", - "psr/http-message": "1.0.*", - "symfony/lock": "dev-master" + "psr/http-message": "1.0.*" }, "require-dev": { "phpunit/phpunit": "4.8.*", @@ -58,11 +57,13 @@ "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", "google/proto-client-php": "^0.12", - "google/gax": "^0.8" + "google/gax": "^0.8", + "symfony/lock": "dev-master#1ba6ac9" }, "suggest": { "google/gax": "Required to support gRPC", - "google/proto-client-php": "Required to support gRPC" + "google/proto-client-php": "Required to support gRPC", + "symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: dev-master#1ba6ac9" }, "autoload": { "psr-4": { diff --git a/docs/contents/cloud-spanner.json b/docs/contents/cloud-spanner.json index f3459b3c5aa0..274b72c15e06 100644 --- a/docs/contents/cloud-spanner.json +++ b/docs/contents/cloud-spanner.json @@ -40,5 +40,8 @@ }, { "title": "Timestamp", "type": "spanner/timestamp" + },{ + "title": "CacheSessionPool", + "type": "spanner/session/cachesessionpool" }] } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index d2c5c1340055..331eaa22a30c 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -596,7 +596,7 @@ public function transaction(array $options = []) * If a callable finishes executing without invoking * {@see Google\Cloud\Spanner\Transaction::commit()} or * {@see Google\Cloud\Spanner\Transaction::rollback()}, the transaction will - * automatically be rolled back and `RuntimeException` thrown. + * automatically be rolled back and `\RuntimeException` thrown. * * Example: * ``` @@ -643,7 +643,7 @@ public function transaction(array $options = []) * `false`. * } * @return mixed The return value of `$operation`. - * @throws RuntimeException + * @throws \RuntimeException */ public function runTransaction(callable $operation, array $options = []) { @@ -1274,7 +1274,7 @@ public function sessionPool() /** * Closes the database connection by returning the active session back to - * the session pool or by deleting the session if there is no pool + * the session pool queue or by deleting the session if there is no pool * associated. * * It is highly important to ensure this is called as it is not always safe @@ -1359,9 +1359,13 @@ public function session($name) */ public function identity() { + $databaseParts = explode('/', $this->name); + $instanceParts = explode('/', $this->instance->name()); + return [ - 'database' => $this->name, - 'instance' => $this->instance->name(), + 'projectId' => $this->projectId, + 'database' => end($databaseParts), + 'instance' => end($instanceParts), ]; } diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index 3e68ce94def5..25a9a7d7dfdb 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -22,7 +22,6 @@ use Google\Cloud\Core\ExponentialBackoff; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; -use Google\Cloud\Spanner\Timestamp; /** * Represent a Cloud Spanner lookup result (either read or executeSql). @@ -43,6 +42,10 @@ class Result implements \IteratorAggregate { const BUFFER_RESULT_LIMIT = 10; + const RETURN_NAME_VALUE_PAIR = 'nameValuePair'; + const RETURN_ASSOCIATIVE = 'associative'; + const RETURN_ZERO_INDEXED = 'zeroIndexed'; + /** * @var array */ @@ -51,7 +54,12 @@ class Result implements \IteratorAggregate /** * @var int */ - private $columnCount; + private $columnCount = 0; + + /** + * @var array|null + */ + private $columnNames; /** * @var ValueMapper @@ -138,9 +146,23 @@ public function __construct( * $rows = $result->rows(); * ``` * + * @param string $format Determines the format in which rows are returned. + * `Result::RETURN_NAME_VALUE_PAIR` returns items as a + * multi-dimensional array containing a name and a value key. + * Ex: `[0 => ['name' => 'column1', 'value' => 'my_value']]`. + * `Result::RETURN_ASSOCIATIVE` returns items as an associative array + * with the column name as the key. Please note with this option, if + * duplicate column names are present a `\RuntimeException` will be + * thrown. `Result::RETURN_ZERO_INDEXED` returns items as a 0 indexed + * array, with the key representing the column number as found by + * executing {@see Google\Cloud\Spanner\Result::columns()}. Ex: + * `[0 => 'my_value']`. **Defaults to** `Result::RETURN_ASSOCIATIVE`. * @return \Generator + * @throws \InvalidArgumentException When an invalid format is provided. + * @throws \RuntimeException When duplicate column names exist with a + * selected format of `Result::RETURN_ASSOCIATIVE`. */ - public function rows() + public function rows($format = self::RETURN_ASSOCIATIVE) { $bufferedResults = []; $call = $this->call; @@ -151,7 +173,7 @@ public function rows() try { $result = $generator->current(); $bufferedResults[] = $result; - $this->setResultData($result); + $this->setResultData($result, $format); if (!isset($result['values'])) { return; @@ -161,7 +183,7 @@ public function rows() list($yieldableRows, $chunkedResult) = $this->parseRowsFromBufferedResults($bufferedResults); foreach ($yieldableRows as $row) { - yield $this->mapper->decodeValues($this->columns, $row); + yield $this->mapper->decodeValues($this->columns, $row, $format); } // Now that we've yielded all available rows, flush the buffer. @@ -179,21 +201,21 @@ public function rows() } $generator->next(); - } catch (\Exception $ex) { - if ($shouldRetry && $ex->getCode() === Grpc\STATUS_UNAVAILABLE) { - $backoff = new ExponentialBackoff($this->retries, function (\Exception $ex) { - return $ex->getCode() === Grpc\STATUS_UNAVAILABLE - ? true - : false; - }); - - // Attempt to resume using our last stored resume token. If we - // successfully resume, flush the buffer. - $generator = $backoff->execute($call, [$this->resumeToken]); - $bufferedResults = []; + } catch (ServiceException $ex) { + if (!$shouldRetry || $ex->getCode() !== Grpc\STATUS_UNAVAILABLE) { + throw $ex; } - throw $ex; + $backoff = new ExponentialBackoff($this->retries, function (ServiceException $ex) { + return $ex->getCode() === Grpc\STATUS_UNAVAILABLE + ? true + : false; + }); + + // Attempt to resume using our last stored resume token. If we + // successfully resume, flush the buffer. + $generator = $backoff->execute($call, [$this->resumeToken]); + $bufferedResults = []; } } @@ -202,11 +224,28 @@ public function rows() list($yieldableRows, $chunkedResult) = $this->parseRowsFromBufferedResults($bufferedResults); foreach ($yieldableRows as $row) { - yield $this->mapper->decodeValues($this->columns, $row); + yield $this->mapper->decodeValues($this->columns, $row, $format); } } } + /** + * Return column names. + * + * Will be populated once the result set is iterated upon. + * + * Example: + * ``` + * $columns = $result->columns(); + * ``` + * + * @return array|null + */ + public function columns() + { + return $this->columnNames; + } + /** * Return result metadata. * @@ -226,6 +265,21 @@ public function metadata() return $this->metadata; } + /** + * Return the session associated with the result stream. + * + * Example: + * ``` + * $session = $result->session(); + * ``` + * + * @return Session + */ + public function session() + { + return $this->session; + } + /** * Get the query plan and execution statistics for the query that produced * this result set. @@ -338,8 +392,10 @@ private function parseRowsFromBufferedResults(array $bufferedResults) /** * @param array $result + * @param string $format + * @throws \RuntimeException */ - private function setResultData(array $result) + private function setResultData(array $result, $format) { $this->stats = isset($result['stats']) ? $result['stats'] @@ -350,9 +406,28 @@ private function setResultData(array $result) } if (isset($result['metadata'])) { + $this->columnNames = []; + $this->columns = []; + $this->columnCount = 0; $this->metadata = $result['metadata']; $this->columns = $result['metadata']['rowType']['fields']; - $this->columnCount = count($this->columns); + + foreach ($this->columns as $key => $column) { + $this->columnNames[] = isset($column['name']) + ? $column['name'] + : $key; + $this->columnCount++; + } + + if ($format === self::RETURN_ASSOCIATIVE + && $this->columnCount !== count(array_unique($this->columnNames)) + ) { + throw new \RuntimeException( + 'Duplicate column names are not supported when returning' . + ' rows in the associative format. Please consider aliasing' . + ' your column names.' + ); + } } if (isset($result['metadata']['transaction']['id'])) { diff --git a/src/Spanner/Session/CacheSessionPool.php b/src/Spanner/Session/CacheSessionPool.php index b67737eec5b5..56e369955506 100644 --- a/src/Spanner/Session/CacheSessionPool.php +++ b/src/Spanner/Session/CacheSessionPool.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Spanner\Session; +use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\Lock\LockInterface; use Google\Cloud\Core\Lock\SymfonyLockAdapter; use Google\Cloud\Spanner\Database; @@ -28,19 +29,47 @@ * This session pool implementation accepts a PSR-6 compatible cache * implementation and utilizes it to store sessions between requests. * - * Please note that if you configure a high minimum session value the first - * request and any after a period of inactivity greater than an hour (the point - * at which sessions will expire) will have an increased amount of latency. This - * is due to the pool attempting to create as many sessions as needed to fill - * itself to match the minimum value. + * Please note that when + * {@see Google\Cloud\Spanner\Session\CacheSessionPool::acquire()} is called at + * most only a single session is created. Due to this, it is possible to sit + * under the minimum session value declared when constructing this instance. In + * order to have the pool match the minimum session value please use the + * {@see Google\Cloud\Spanner\Session\CacheSessionPool::warmup()} method. This + * will create as many sessions as needed to match the minimum value, and is the + * recommended way to bootstrap the session pool. * - * For this reason, it is highly recommended to configure a script to make an - * initial request to warm up the pool and manage subsequent requests during - * off-peak hours to keep the pool active. + * Sessions are created on demand up to the maximum session value set during + * instantiation of the pool. After peak usage hours, you may find that more + * sessions are available than your demand may require. It is important to make + * sure the number of active sessions managed by the Spanner backend is kept + * as minimal as possible. In order to help maintain this balance, please use + * the {@see Google\Cloud\Spanner\Session\CacheSessionPool::downsize()} method + * on an interval that matches when you expect to see a decrease in traffic. + * This will help ensure you never run into issues where the Spanner backend is + * locked up after having met the maximum number of sessions assigned per node + * (The current maximum sessions per node is 10k). + * + * Additionally, when expecting a long period of inactivity (such as a + * maintenance window), please make sure to call + * {@see Google\Cloud\Spanner\Session\CacheSessionPool::clear()} in order to + * delete any active sessions. * * Please note: While required for the session pool, a PSR-6 implementation is * not included in this library. It will be neccesary to include a separate - * dependency to fulfill this requirement. + * dependency to fulfill this requirement. The below example makes use of + * [Symfony's Cache Component](https://github.com/symfony/cache). For more + * implementations please see the + * [Packagist PHP Package Repository](https://packagist.org/providers/psr/cache-implementation). + * + * Furthermore, [Symfony's Lock Component](https://github.com/symfony/lock) is + * also required to be installed as a separate dependency. In our current alpha + * state with Spanner we are relying on the following dev commit: + * + * `composer require symfony/lock:dev-master#1ba6ac9` + * + * As development continues, this dependency on a dev-master branch will be + * discontinued. Please also note, since this is a dev-master dependency it may + * require modifications to your composer minimum-stability settings. * * Example: * ``` @@ -59,7 +88,10 @@ */ class CacheSessionPool implements SessionPoolInterface { - const CACHE_KEY_TEMPLATE = 'cache-session-pool.%s.%s'; + const CACHE_KEY_TEMPLATE = 'cache-session-pool.%s.%s.%s'; + + const DURATION_TWENTY_MINUTES = 1200; + const DURATION_ONE_MINUTE = 60; /** * @var array @@ -99,7 +131,7 @@ class CacheSessionPool implements SessionPoolInterface * Configuration Options. * * @type int $maxSessions The maximum number of sessions to store in the - * pool. **Defaults to** PHP_INT_MAX. + * pool. **Defaults to** `500`. * @type int $minSessions The minimum number of sessions to store in the * pool. **Defaults to** `1`. * @type bool $shouldWaitForSession If the pool is full, whether to block @@ -142,7 +174,7 @@ public function acquire($context = SessionPoolInterface::CONTEXT_READ) list($session, $toCreate) = $this->config['lock']->synchronize(function () { $toCreate = []; $session = null; - $shouldSave = false; + $shouldSave = true; $item = $this->cacheItemPool->getItem($this->cacheKey); $data = (array) $item->get() ?: $this->initialize(); @@ -152,51 +184,57 @@ public function acquire($context = SessionPoolInterface::CONTEXT_READ) // for more. if ($data['queue']) { $session = $this->getSession($data); - $shouldSave = true; } elseif ($this->config['maxSessions'] <= $this->getSessionCount($data)) { $this->purgeOrphanedInUseSessions($data); $this->purgeOrphanedToCreateItems($data); - $shouldSave = true; + $session = $this->getSession($data); } - $toCreate = $this->buildToCreateList($data, is_array($session)); - $data['toCreate'] += $toCreate; + if (!$session) { + $count = $this->getSessionCount($data); + + if ($count < $this->config['maxSessions']) { + $toCreate = $this->buildToCreateList(1); + $data['toCreate'] += $toCreate; + } else { + $shouldSave = false; + } + } - if ($shouldSave || $toCreate) { + if ($shouldSave) { $this->cacheItemPool->save($item->set($data)); } return [$session, $toCreate]; }); - // Create sessions if needed. + // Create a session if needed. if ($toCreate) { $createdSessions = []; $exception = null; try { $createdSessions = $this->createSessions(count($toCreate)); - } catch (\Exception $ex) { - $exception = $ex; + } catch (\Exception $exception) { } $session = $this->config['lock']->synchronize(function () use ( - $session, $toCreate, $createdSessions, $exception ) { + $session = null; $item = $this->cacheItemPool->getItem($this->cacheKey); $data = $item->get(); $data['queue'] = array_merge($data['queue'], $createdSessions); - // Now that we've created the sessions, we can remove them from + // Now that we've created the session, we can remove it from // the list of intent. foreach ($toCreate as $id => $time) { unset($data['toCreate'][$id]); } - if (!$session && !$exception) { + if (!$exception) { $session = array_shift($data['queue']); $data['inUse'][$session['name']] = $session + [ @@ -252,12 +290,203 @@ public function release(Session $session) } /** - * Clear the session pool. Please note that this simply removes sessions - * data from the cache and does not delete the sessions themselves. + * Keeps a checked out session alive. + * + * In use sessions that have not had their `lastActive` time updated + * in the last 20 minutes will be considered eligible to be moved back into + * the queue if the max sessions value has been met. In order to work around + * this when performing a large streaming execute or read call please make + * sure to call this method roughly every 15 minutes between reading rows + * to keep your session active. + * + * @param Session $session The session to keep alive. + */ + public function keepAlive(Session $session) + { + $this->config['lock']->synchronize(function () use ($session) { + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = $item->get(); + $data['inUse'][$session->name()]['lastActive'] = $this->time(); + + $this->cacheItemPool->save($item->set($data)); + }); + } + + /** + * Downsizes the queue of available sessions by the given percentage. This is + * relative to the minimum sessions value. For example: Assuming a full + * queue, with maximum sessions of 10 and a minimum of 2, downsizing by 50% + * would leave 6 sessions in the queue. The count of items to be deleted will + * be rounded up in the case of a fraction. + * + * A session may be removed from the cache, but still tracked as active by + * the Spanner backend if a delete operation failed. To ensure you do not + * exceed the maximum number of sessions available per node, please be sure + * to check the return value of this method to be certain all sessions have + * been deleted. + * + * Please note this method will attempt to synchronously delete sessions and + * will block until complete. + * + * @param int $percent The percentage to downsize the pool by. Must be + * between 1 and 100. + * @return array An associative array containing a key `deleted` which holds + * an integer value representing the number of queued sessions + * deleted on the backend and a key `failed` which holds a list of + * queued {@see Google\Cloud\Spanner\Session\Session} objects which + * failed to delete. + * @throws \InvaldArgumentException + */ + public function downsize($percent) + { + if ($percent < 1 || 100 < $percent) { + throw new \InvalidArgumentException('The provided percent must be between 1 and 100.'); + } + + $failed = []; + $toDelete = $this->config['lock']->synchronize(function () use ($percent) { + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = (array) $item->get() ?: $this->initialize(); + $toDelete = []; + $queueCount = count($data['queue']); + $availableCount = max($queueCount - $this->config['minSessions'], 0); + $countToDelete = ceil($availableCount * ($percent * 0.01)); + + if ($countToDelete) { + $toDelete = array_splice($data['queue'], (int) -$countToDelete); + } + + $this->cacheItemPool->save($item->set($data)); + return $toDelete; + }); + + foreach ($toDelete as $sessionData) { + $session = $this->database->session($sessionData['name']); + + try { + $session->delete(); + } catch (\Exception $ex) { + if ($ex instanceof NotFoundException) { + continue; + } + + $failed[] = $session; + } + } + + return [ + 'deleted' => count($toDelete) - count($failed), + 'failed' => $failed + ]; + } + + /** + * Create enough sessions to meet the minimum session constraint. + * + * @return int The number of sessions created and added to the queue. + */ + public function warmup() + { + $toCreate = $this->config['lock']->synchronize(function () { + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = (array) $item->get() ?: $this->initialize(); + $count = $this->getSessionCount($data); + $toCreate = []; + + if ($count < $this->config['minSessions']) { + $toCreate = $this->buildToCreateList($this->config['minSessions'] - $count); + $data['toCreate'] += $toCreate; + $this->cacheItemPool->save($item->set($data)); + } + + return $toCreate; + }); + + if (!$toCreate) { + return 0; + } + + $createdSessions = []; + $exception = null; + + try { + $createdSessions = $this->createSessions(count($toCreate)); + } catch (\Exception $exception) { + } + + $this->config['lock']->synchronize(function () use ($toCreate, $createdSessions) { + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = $item->get(); + $data['queue'] = array_merge($data['queue'], $createdSessions); + + // Now that we've created the sessions, we can remove them from + // the list of intent. + foreach ($toCreate as $id => $time) { + unset($data['toCreate'][$id]); + } + + $this->cacheItemPool->save($item->set($data)); + }); + + if ($exception) { + throw $exception; + } + + return count($toCreate); + } + + /** + * Clear the cache and attempt to delete all sessions in the pool. + * + * A session may be removed from the cache, but still tracked as active by + * the Spanner backend if a delete operation failed. To ensure you do not + * exceed the maximum number of sessions available per node, please be sure + * to check the return value of this method to be certain all sessions have + * been deleted. + * + * Please note this method will attempt to synchronously delete sessions and + * will block until complete. + * + * @return array An array containing a list of + * {@see Google\Cloud\Spanner\Session\Session} objects which failed + * to delete. */ public function clear() { - $this->cacheItemPool->clear(); + $failed = []; + $sessions = $this->config['lock']->synchronize(function () { + $sessions = []; + $item = $this->cacheItemPool->getItem($this->cacheKey); + $data = (array) $item->get() ?: $this->initialize(); + + foreach ($data['queue'] as $session) { + $sessions[] = $session['name']; + } + + foreach ($data['inUse'] as $session) { + $sessions[] = $session['name']; + } + + $this->cacheItemPool->clear(); + + return $sessions; + }); + + foreach ($sessions as $sessionName) { + $session = $this->database->session($sessionName); + + try { + $session->delete(); + } catch (\Exception $ex) { + if ($ex instanceof NotFoundException) { + continue; + } + + $failed[] = $session; + } + } + + return $failed; } /** @@ -269,7 +498,12 @@ public function setDatabase(Database $database) { $this->database = $database; $identity = $database->identity(); - $this->cacheKey = sprintf(self::CACHE_KEY_TEMPLATE, $identity['instance'], $identity['database']); + $this->cacheKey = sprintf( + self::CACHE_KEY_TEMPLATE, + $identity['projectId'], + $identity['instance'], + $identity['database'] + ); } /** @@ -296,22 +530,13 @@ protected function time() * Builds out a list of timestamps indicating the start time of the intent * to create a session. * - * @param array $data - * @param bool $hasSession + * @param int $number * @return array */ - private function buildToCreateList(array $data, $hasSession) + private function buildToCreateList($number) { - $number = 0; $toCreate = []; $time = $this->time(); - $count = $this->getSessionCount($data); - - if ($count < $this->config['minSessions']) { - $number = $this->config['minSessions'] - $count; - } elseif (!$hasSession && !$data['queue'] && $count < $this->config['maxSessions']) { - $number++; - } for ($i = 0; $i < $number; $i++) { $toCreate[uniqid($time . '_')] = $time; @@ -329,21 +554,29 @@ private function buildToCreateList(array $data, $hasSession) private function purgeOrphanedToCreateItems(array &$data) { foreach ($data['toCreate'] as $key => $timestamp) { - if ($timestamp + 1200 < $this->time()) { + $time = $this->time(); + + if ($timestamp + self::DURATION_TWENTY_MINUTES < $this->time()) { unset($data['toCreate'][$key]); } } } /** - * Purge any in use sessions that have been inactive for 20 minutes or more. + * Purges in use sessions. If a session was last active an hour ago, we + * assume it is expired and remove it from the pool. If last active 20 + * minutes ago, we attempt to return the session back to the queue. * * @param array $data */ private function purgeOrphanedInUseSessions(array &$data) { foreach ($data['inUse'] as $key => $session) { - if ($session['lastActive'] + 1200 < $this->time()) { + if ($session['lastActive'] + SessionPoolInterface::SESSION_EXPIRATION_SECONDS < $this->time()) { + unset($data['inUse'][$key]); + } elseif ($session['lastActive'] + self::DURATION_TWENTY_MINUTES < $this->time()) { + unset($session['lastActive']); + array_push($data['queue'], $session); unset($data['inUse'][$key]); } } @@ -392,7 +625,7 @@ private function getSession(array &$data) $session = array_shift($data['queue']); if ($session) { - if ($session['expiration'] - 60 < $this->time()) { + if ($session['expiration'] - self::DURATION_ONE_MINUTE < $this->time()) { return $this->getSession($data); } @@ -510,9 +743,21 @@ private function waitForNextAvailableSession() * Get the default lock. * * @return LockInterface + * @throws \RunTimeException */ private function getDefaultLock() { + if (!class_exists(FlockStore::class)) { + throw new \RuntimeException( + 'The symfony/lock component must be installed in order for ' . + 'a default lock to be assumed. Please run the following from ' . + 'the command line: composer require symfony/lock:dev-master#1ba6ac9. ' . + 'Please note, since this is a dev-master dependency it may ' . + 'require modifications to your composer minimum-stability ' . + 'settings.' + ); + } + $store = new FlockStore(sys_get_temp_dir()); return new SymfonyLockAdapter( diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index f6a2a238e53e..ca3d57c54998 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -144,27 +144,43 @@ public function encodeValuesAsSimpleType(array $values) * * @param array $columns The list of columns. * @param array $row The row data. + * @param string $format The format in which to return the rows. * @return array The decoded row data. + * @throws \InvalidArgumentException */ - public function decodeValues(array $columns, array $row) + public function decodeValues(array $columns, array $row, $format) { - $cols = []; - $types = []; - - foreach ($columns as $index => $column) { - $cols[] = (isset($column['name'])) - ? $column['name'] - : $index; - $types[] = $column['type']; - } + switch ($format) { + case Result::RETURN_NAME_VALUE_PAIR: + foreach ($row as $index => $value) { + $row[$index] = [ + 'name' => $this->getColumnName($columns, $index), + 'value' => $this->decodeValue($value, $columns[$index]['type']) + ]; + } - $res = []; - foreach ($row as $index => $value) { - $i = $cols[$index]; - $res[$i] = $this->decodeValue($value, $types[$index]); - } + return $row; - return $res; + case Result::RETURN_ASSOCIATIVE: + foreach ($row as $index => $value) { + unset($row[$index]); + $row[$this->getColumnName($columns, $index)] = $this->decodeValue( + $value, + $columns[$index]['type'] + ); + } + + return $row; + + case Result::RETURN_ZERO_INDEXED: + foreach ($row as $index => $value) { + $row[$index] = $this->decodeValue($value, $columns[$index]['type']); + } + + return $row; + default: + throw new \InvalidArgumentException('Invalid format provided.'); + } } /** @@ -226,7 +242,7 @@ private function decodeValue($value, array $type) break; case self::TYPE_STRUCT: - $value = $this->decodeValues($type['structType']['fields'], $value); + $value = $this->decodeValues($type['structType']['fields'], $value, Result::RETURN_ASSOCIATIVE); break; case self::TYPE_FLOAT64: @@ -397,4 +413,11 @@ private function typeObject($type, array $nestedDefinition = [], $nestedDefiniti $nestedDefinitionType => $nestedDefinition ]); } + + private function getColumnName($columns, $index) + { + return isset($columns[$index]['name']) + ? $columns[$index]['name'] + : $index; + } } diff --git a/tests/snippets/Spanner/ResultTest.php b/tests/snippets/Spanner/ResultTest.php index 766525d0c176..acff7135e19e 100644 --- a/tests/snippets/Spanner/ResultTest.php +++ b/tests/snippets/Spanner/ResultTest.php @@ -44,6 +44,10 @@ public function setUp() ->willReturn($this->resultGenerator()); $result->metadata() ->willReturn([]); + $result->columns() + ->willReturn([]); + $result->session() + ->willReturn($this->prophesize(Session::class)->reveal()); $result->snapshot() ->willReturn($this->prophesize(Snapshot::class)->reveal()); $result->transaction() @@ -77,6 +81,14 @@ public function testRows() $this->assertInstanceOf(\Generator::class, $res->returnVal()); } + public function testColumns() + { + $snippet = $this->snippetFromMethod(Result::class, 'columns'); + $snippet->addLocal('result', $this->result); + $res = $snippet->invoke('columns'); + $this->assertInternalType('array', $res->returnVal()); + } + public function testMetadata() { $snippet = $this->snippetFromMethod(Result::class, 'metadata'); @@ -85,6 +97,14 @@ public function testMetadata() $this->assertInternalType('array', $res->returnVal()); } + public function testSession() + { + $snippet = $this->snippetFromMethod(Result::class, 'session'); + $snippet->addLocal('result', $this->result); + $res = $snippet->invoke('session'); + $this->assertInstanceOf(Session::class, $res->returnVal()); + } + public function testStats() { $snippet = $this->snippetFromMethod(Result::class, 'stats'); diff --git a/tests/unit/Spanner/ResultTest.php b/tests/unit/Spanner/ResultTest.php index 173468c00ada..e62aaab4ee8d 100644 --- a/tests/unit/Spanner/ResultTest.php +++ b/tests/unit/Spanner/ResultTest.php @@ -17,8 +17,13 @@ namespace Google\Cloud\Tests\Unit\Spanner; -use Google\Cloud\Spanner\Snapshot; +use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Spanner\Transaction; +use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Session\Session; +use Google\Cloud\Spanner\Snapshot; +use Google\Cloud\Spanner\ValueMapper; +use Prophecy\Argument; /** * @group spanner @@ -27,12 +32,30 @@ class ResultTest extends \PHPUnit_Framework_TestCase { use ResultTestTrait; + private $metadata = [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'f1', + 'type' => 6 + ] + ] + ] + ]; + + public function setUp() + { + if (!extension_loaded('grpc')) { + $this->markTestSkipped('Must have the grpc extension installed to run this test.'); + } + } + /** * @dataProvider streamingDataProvider */ public function testRows($chunks, $expectedValues) { - $result = iterator_to_array($this->getResultClass($chunks)); + $result = iterator_to_array($this->getResultClass($chunks)->rows()); $this->assertEquals($expectedValues, $result); } @@ -45,6 +68,86 @@ public function testIterator() $this->assertEquals($fixture['result']['value'], $result); } + public function testResumesBrokenStream() + { + $timesCalled = 0; + $chunks = [ + [ + 'metadata' => $this->metadata, + 'values' => ['a'] + ], + [ + 'values' => ['b'], + 'resumeToken' => 'abc' + ], + [ + 'values' => ['c'] + ] + ]; + + $result = $this->getResultClass( + null, + 'r', + null, + function () use ($chunks, &$timesCalled) { + $timesCalled++; + + foreach ($chunks as $key => $chunk) { + if ($timesCalled === 1 && $key === 2) { + throw new ServiceException('Unavailable', 14); + } + yield $chunk; + } + + } + ); + iterator_to_array($result->rows()); + $this->assertEquals(2, $timesCalled); + } + + /** + * @expectedException Google\Cloud\Core\Exception\ServiceException + */ + public function testThrowsExceptionWhenCannotRetry() + { + $chunks = [ + [ + 'metadata' => $this->metadata, + 'values' => ['a'] + ], + [ + 'values' => ['b'] + ] + ]; + + $result = $this->getResultClass( + null, + 'r', + null, + function () use ($chunks) { + foreach ($chunks as $key => $chunk) { + if ($key === 1) { + throw new ServiceException('Should not retry this.'); + } + yield $chunk; + } + + } + ); + iterator_to_array($result->rows()); + } + + public function testColumns() + { + $fixture = $this->getStreamingDataFixture()['tests'][0]; + $result = $this->getResultClass($fixture['chunks']); + $expectedColumnNames = ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7']; + + $this->assertNull($result->columns()); + $result->rows()->next(); + $this->assertEquals($expectedColumnNames, $result->columns()); + } + public function testMetadata() { $fixture = $this->getStreamingDataFixture()['tests'][0]; @@ -56,6 +159,14 @@ public function testMetadata() $this->assertEquals($expectedMetadata, $result->metadata()); } + public function testSession() + { + $fixture = $this->getStreamingDataFixture()['tests'][0]; + $result = $this->getResultClass($fixture['chunks']); + + $this->assertInstanceOf(Session::class, $result->session()); + } + public function testStats() { $fixture = $this->getStreamingDataFixture()['tests'][1]; @@ -86,4 +197,44 @@ public function testSnapshot() $result->rows()->next(); $this->assertInstanceOf(Snapshot::class, $result->snapshot()); } + + public function testUsesCorrectDefaultFormatOption() + { + $mapper = $this->prophesize(ValueMapper::class); + $mapper->decodeValues( + Argument::any(), + Argument::any(), + 'nameValuePair' + ); + $result = $this->getResultClass([], 'r', $mapper->reveal()); + + $rows = $result->rows(); + $rows->current(); + } + + /** + * @dataProvider formatProvider + */ + public function testRecievesCorrectFormatOption($format) + { + $mapper = $this->prophesize(ValueMapper::class); + $mapper->decodeValues( + Argument::any(), + Argument::any(), + $format + ); + $result = $this->getResultClass([], 'r', $mapper->reveal()); + + $rows = $result->rows($format); + $rows->current(); + } + + public function formatProvider() + { + return [ + ['nameValuePair'], + ['associative'], + ['zeroIndexed'] + ]; + } } diff --git a/tests/unit/Spanner/ResultTestTrait.php b/tests/unit/Spanner/ResultTestTrait.php index b40d1d270cdc..e260f110c8d5 100644 --- a/tests/unit/Spanner/ResultTestTrait.php +++ b/tests/unit/Spanner/ResultTestTrait.php @@ -42,19 +42,34 @@ public function streamingDataProviderFirstChunk() } } - private function getResultClass($chunks, $context = 'r') - { + private function getResultClass( + $chunks = null, + $context = 'r', + $mapper = null, + $call = null + ) { $operation = $this->prophesize(Operation::class); $session = $this->prophesize(Session::class)->reveal(); - $mapper = $this->prophesize(ValueMapper::class); $transaction = $this->prophesize(Transaction::class); $snapshot = $this->prophesize(Snapshot::class); - $mapper->decodeValues( - Argument::any(), - Argument::any() - )->will(function ($args) { - return $args[1]; - }); + + if (!$mapper) { + $mapper = $this->prophesize(ValueMapper::class); + $mapper->decodeValues( + Argument::any(), + Argument::any(), + Argument::any() + )->will(function ($args) { + return $args[1]; + }); + $mapper = $mapper->reveal(); + } + + if (!$call) { + $call = function () use ($chunks) { + return $this->resultGenerator($chunks); + }; + } if ($context === 'r') { $operation->createSnapshot( @@ -71,11 +86,9 @@ private function getResultClass($chunks, $context = 'r') return new Result( $operation->reveal(), $session, - function () use ($chunks) { - return $this->resultGenerator($chunks); - }, + $call, $context, - $mapper->reveal() + $mapper ); } diff --git a/tests/unit/Spanner/Session/CacheSessionPoolTest.php b/tests/unit/Spanner/Session/CacheSessionPoolTest.php index 8391af10b88e..0083162dc72c 100644 --- a/tests/unit/Spanner/Session/CacheSessionPoolTest.php +++ b/tests/unit/Spanner/Session/CacheSessionPoolTest.php @@ -31,7 +31,8 @@ */ class CacheSessionPoolTest extends \PHPUnit_Framework_TestCase { - const CACHE_KEY_TEMPLATE = 'cache-session-pool.%s.%s'; + const CACHE_KEY_TEMPLATE = 'cache-session-pool.%s.%s.%s'; + const PROJECT_ID = 'project'; const DATABASE_NAME = 'database'; const INSTANCE_NAME = 'instance'; @@ -135,7 +136,7 @@ public function testAcquireRemovesToCreateItemsIfCreateCallFails() $actualItemPool = $pool->cacheItemPool(); $actualCacheData = $actualItemPool->getItem( - sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + sprintf(self::CACHE_KEY_TEMPLATE, self::PROJECT_ID, self::INSTANCE_NAME, self::DATABASE_NAME) )->get(); $this->assertEmpty($actualCacheData['toCreate']); @@ -175,19 +176,165 @@ public function testRelease() $pool->release($session->reveal()); $actualItemPool = $pool->cacheItemPool(); $actualCacheData = $actualItemPool->getItem( - sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + sprintf(self::CACHE_KEY_TEMPLATE, self::PROJECT_ID, self::INSTANCE_NAME, self::DATABASE_NAME) )->get(); $this->assertEquals($expectedCacheData, $actualCacheData); } + public function testKeepAlive() + { + $sessionName = 'alreadyCheckedOut'; + $lastActiveOriginal = 1000; + $session = $this->prophesize(Session::class); + $session->name() + ->willReturn($sessionName); + $pool = new CacheSessionPoolStub($this->getCacheItemPool([ + 'queue' => [], + 'inUse' => [ + $sessionName => [ + 'name' => $sessionName, + 'expiration' => $this->time + 3600, + 'lastActive' => $lastActiveOriginal + ] + ], + 'toCreate' => [] + ]), [], $this->time); + $pool->setDatabase($this->getDatabase()); + $actualItemPool = $pool->cacheItemPool(); + $actualCacheData = $actualItemPool->getItem( + sprintf(self::CACHE_KEY_TEMPLATE, self::PROJECT_ID, self::INSTANCE_NAME, self::DATABASE_NAME) + )->get(); + + $this->assertEquals($lastActiveOriginal, $actualCacheData['inUse'][$sessionName]['lastActive']); + + $pool->keepAlive($session->reveal()); + $actualCacheData = $actualItemPool->getItem( + sprintf(self::CACHE_KEY_TEMPLATE, self::PROJECT_ID, self::INSTANCE_NAME, self::DATABASE_NAME) + )->get(); + + $this->assertEquals($this->time, $actualCacheData['inUse'][$sessionName]['lastActive']); + } + + /** + * @dataProvider downsizeDataProvider + */ + public function testDownsizeDeletes($percent, $expectedDeleteCount) + { + $time = time() + 3600; + $pool = new CacheSessionPoolStub($this->getCacheItemPool([ + 'queue' => [ + [ + 'name' => 'session0', + 'expiration' => $time + ], + [ + 'name' => 'session1', + 'expiration' => $time + ], + [ + 'name' => 'session2', + 'expiration' => $time + ], + [ + 'name' => 'session3', + 'expiration' => $time + ], + [ + 'name' => 'session4', + 'expiration' => $time + ] + ], + 'inUse' => [], + 'toCreate' => [] + ])); + $pool->setDatabase($this->getDatabase(false, true)); + $response = $pool->downsize($percent); + + $this->assertEquals($expectedDeleteCount, $response['deleted']); + } + + public function downsizeDataProvider() + { + return [ + [50, 2], + [1, 1], + [100, 4] + ]; + } + + public function testDownsizeFails() + { + $time = time() + 3600; + $pool = new CacheSessionPoolStub($this->getCacheItemPool([ + 'queue' => [ + [ + 'name' => 'session0', + 'expiration' => $time + ], + [ + 'name' => 'session1', + 'expiration' => $time + ] + ], + 'inUse' => [], + 'toCreate' => [] + ])); + $pool->setDatabase($this->getDatabase()); + $response = $pool->downsize(100); + + $this->assertEquals(0, $response['deleted']); + $this->assertEquals(1, count($response['failed'])); + $this->assertContainsOnlyInstancesOf(Session::class, $response['failed']); + } + + /** + * @dataProvider invalidPercentDownsizeDataProvider + */ + public function testDownsizeThrowsExceptionWithInvalidPercent($percent) + { + $pool = new CacheSessionPoolStub($this->getCacheItemPool()); + $exceptionThrown = false; + + try { + $pool->downsize($percent); + } catch (\InvalidArgumentException $ex) { + $exceptionThrown = true; + } + + $this->assertTrue($exceptionThrown); + } + + public function invalidPercentDownsizeDataProvider() + { + return [ + [-1], + [0], + [101] + ]; + } + + public function testWarmup() + { + $expectedCreationCount = 5; + $pool = new CacheSessionPoolStub( + $this->getCacheItemPool(), + ['minSessions' => $expectedCreationCount] + ); + $pool->setDatabase($this->getDatabase()); + $response = $pool->warmup(); + + $this->assertEquals($expectedCreationCount, $response); + } + public function testClearPool() { $pool = new CacheSessionPoolStub($this->getCacheItemPool()); + $pool->setDatabase($this->getDatabase()); $pool->clear(); $actualItemPool = $pool->cacheItemPool(); $actualCacheData = $actualItemPool->getItem( - sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + sprintf(self::CACHE_KEY_TEMPLATE, self::PROJECT_ID, self::INSTANCE_NAME, self::DATABASE_NAME) )->get(); $this->assertNull($actualCacheData); @@ -203,7 +350,7 @@ public function testAcquire($config, $cacheData, $expectedCacheData, $time) $actualSession = $pool->acquire(); $actualItemPool = $pool->cacheItemPool(); $actualCacheData = $actualItemPool->getItem( - sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + sprintf(self::CACHE_KEY_TEMPLATE, self::PROJECT_ID, self::INSTANCE_NAME, self::DATABASE_NAME) )->get(); $this->assertInstanceOf(Session::class, $actualSession); @@ -232,37 +379,24 @@ public function acquireDataProvider() ], $time ], - // Set #1: Purge expired session from queue and create sessions up to min + // Set #1: Purge expired session from queue and create [ - ['minSessions' => 3], + ['minSessions' => 1], [ 'queue' => [ [ 'name' => 'expired', 'expiration' => $time - 3000 - ], - [ - 'name' => 'stillValid', - 'expiration' => $time + 3600 ] ], 'inUse' => [], 'toCreate' => [] ], [ - 'queue' => [ - [ - 'name' => 'session0', - 'expiration' => $time + 3600 - ], - [ - 'name' => 'session1', - 'expiration' => $time + 3600 - ] - ], + 'queue' => [], 'inUse' => [ - 'stillValid' => [ - 'name' => 'stillValid', + 'session0' => [ + 'name' => 'session0', 'expiration' => $time + 3600, 'lastActive' => $time ] @@ -271,8 +405,8 @@ public function acquireDataProvider() ], $time ], - // // Set #2: Create a new session when all available are checked out - // // and we have not reached the max limit + // Set #2: Create a new session when all available are checked out + // and we have not reached the max limit [ [], [ @@ -318,7 +452,7 @@ public function acquireDataProvider() 'expiredInUse2' => [ 'name' => 'expiredInUse2', 'expiration' => $time - 5000, - 'lastActive' => $time - 1201 + 'lastActive' => $time - 3601 ] ], 'toCreate' => [ @@ -393,11 +527,38 @@ public function acquireDataProvider() 'toCreate' => [] ], $time - ] + ], + // Set #6: Return inactive in use session back to queue + [ + ['maxSessions' => 1], + [ + 'queue' => [], + 'inUse' => [ + 'inactiveInUse1' => [ + 'name' => 'inactiveInUse1', + 'expiration' => $time + 3600, + 'lastActive' => $time - 1201 + ] + ], + 'toCreate' => [] + ], + [ + 'queue' => [], + 'inUse' => [ + 'inactiveInUse1' => [ + 'name' => 'inactiveInUse1', + 'expiration' => $time + 3600, + 'lastActive' => $time + ] + ], + 'toCreate' => [] + ], + $time + ], ]; } - private function getDatabase($shouldCreateThrowException = false) + private function getDatabase($shouldCreateThrowException = false, $willDeleteSessions = false) { $database = $this->prophesize(Database::class); $createdSession = $this->prophesize(Session::class); @@ -408,6 +569,11 @@ private function getDatabase($shouldCreateThrowException = false) ->willReturn($this->time + 3600); $session->exists() ->willReturn(false); + + if ($willDeleteSessions) { + $session->delete() + ->willReturn(null); + } $database->session(Argument::any()) ->will(function ($args) use ($session) { $session->name() @@ -417,6 +583,7 @@ private function getDatabase($shouldCreateThrowException = false) }); $database->identity() ->willReturn([ + 'projectId' => self::PROJECT_ID, 'database' => self::DATABASE_NAME, 'instance' => self::INSTANCE_NAME ]); @@ -446,7 +613,7 @@ private function getCacheItemPool(array $cacheData = null) { $cacheItemPool = new MemoryCacheItemPool(); $cacheItem = $cacheItemPool->getItem( - sprintf(self::CACHE_KEY_TEMPLATE, self::INSTANCE_NAME, self::DATABASE_NAME) + sprintf(self::CACHE_KEY_TEMPLATE, self::PROJECT_ID, self::INSTANCE_NAME, self::DATABASE_NAME) ); $cacheItemPool->save($cacheItem->set($cacheData)); @@ -466,6 +633,6 @@ public function __construct(CacheItemPoolInterface $cacheItemPool, array $config protected function time() { - return $this->time ?: parent::$this->time(); + return $this->time ?: parent::time(); } } diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php index 614860f9dde4..e59def509444 100644 --- a/tests/unit/Spanner/ValueMapperTest.php +++ b/tests/unit/Spanner/ValueMapperTest.php @@ -20,6 +20,7 @@ use Google\Cloud\Core\Int64; use Google\Cloud\Spanner\Bytes; use Google\Cloud\Spanner\Date; +use Google\Cloud\Spanner\Result; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\ValueMapper; @@ -28,6 +29,8 @@ */ class ValueMapperTest extends \PHPUnit_Framework_TestCase { + const FORMAT_TEST_VALUE = 'abc'; + private $mapper; public function setUp() @@ -171,11 +174,63 @@ public function testEncodeValuesAsSimpleType() $this->assertEquals($vals['array'], $res[10]); } + /** + * @expectedException \InvalidArgumentException + */ + public function testDecodeValuesThrowsExceptionWithInvalidFormat() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_STRING), + $this->createRow(self::FORMAT_TEST_VALUE), + 'Not a real format' + ); + } + + /** + * @dataProvider formatProvider + */ + public function testDecodeValuesReturnsVariedFormats($expectedOutput, $format) + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_STRING), + $this->createRow(self::FORMAT_TEST_VALUE), + $format + ); + + $this->assertEquals($expectedOutput, $res); + } + + public function formatProvider() + { + return [ + [ + ['rowName' => self::FORMAT_TEST_VALUE], + Result::RETURN_ASSOCIATIVE + ], + [ + [ + [ + 'name' => 'rowName', + 'value' => self::FORMAT_TEST_VALUE + ] + ], + Result::RETURN_NAME_VALUE_PAIR + ], + [ + [ + 0 => self::FORMAT_TEST_VALUE + ], + Result::RETURN_ZERO_INDEXED + ], + ]; + } + public function testDecodeValuesBool() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_BOOL), - $this->createRow(false) + $this->createRow(false), + Result::RETURN_ASSOCIATIVE ); $this->assertEquals(false, $res['rowName']); } @@ -184,7 +239,8 @@ public function testDecodeValuesInt() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_INT64), - $this->createRow('555') + $this->createRow('555'), + Result::RETURN_ASSOCIATIVE ); $this->assertEquals(555, $res['rowName']); } @@ -194,7 +250,8 @@ public function testDecodeValuesInt64Object() $mapper = new ValueMapper(true); $res = $mapper->decodeValues( $this->createField(ValueMapper::TYPE_INT64), - $this->createRow('555') + $this->createRow('555'), + Result::RETURN_ASSOCIATIVE ); $this->assertInstanceOf(Int64::class, $res['rowName']); $this->assertEquals('555', $res['rowName']->get()); @@ -204,7 +261,8 @@ public function testDecodeValuesFloat() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_FLOAT64), - $this->createRow(3.1415) + $this->createRow(3.1415), + Result::RETURN_ASSOCIATIVE ); $this->assertEquals(3.1415, $res['rowName']); } @@ -213,7 +271,8 @@ public function testDecodeValuesFloatNaN() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_FLOAT64), - $this->createRow('NaN') + $this->createRow('NaN'), + Result::RETURN_ASSOCIATIVE ); $this->assertTrue(is_nan($res['rowName'])); } @@ -222,7 +281,8 @@ public function testDecodeValuesFloatInfinity() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_FLOAT64), - $this->createRow('Infinity') + $this->createRow('Infinity'), + Result::RETURN_ASSOCIATIVE ); $this->assertTrue(is_infinite($res['rowName'])); @@ -233,7 +293,8 @@ public function testDecodeValuesFloatNegativeInfinity() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_FLOAT64), - $this->createRow('-Infinity') + $this->createRow('-Infinity'), + Result::RETURN_ASSOCIATIVE ); $this->assertTrue(is_infinite($res['rowName'])); @@ -247,7 +308,8 @@ public function testDecodeValuesFloatError() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_FLOAT64), - $this->createRow('foo') + $this->createRow('foo'), + Result::RETURN_ASSOCIATIVE ); } @@ -255,7 +317,8 @@ public function testDecodeValuesString() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_STRING), - $this->createRow('foo') + $this->createRow('foo'), + Result::RETURN_ASSOCIATIVE ); $this->assertEquals('foo', $res['rowName']); } @@ -265,7 +328,8 @@ public function testDecodeValuesTimestamp() $dt = new \DateTime; $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_TIMESTAMP), - $this->createRow($dt->format(Timestamp::FORMAT)) + $this->createRow($dt->format(Timestamp::FORMAT)), + Result::RETURN_ASSOCIATIVE ); $this->assertInstanceOf(Timestamp::class, $res['rowName']); @@ -277,7 +341,8 @@ public function testDecodeValuesDate() $dt = new \DateTime; $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_DATE), - $this->createRow($dt->format(Date::FORMAT)) + $this->createRow($dt->format(Date::FORMAT)), + Result::RETURN_ASSOCIATIVE ); $this->assertInstanceOf(Date::class, $res['rowName']); @@ -288,7 +353,8 @@ public function testDecodeValuesBytes() { $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_BYTES), - $this->createRow(base64_encode('hello world')) + $this->createRow(base64_encode('hello world')), + Result::RETURN_ASSOCIATIVE ); $this->assertInstanceOf(Bytes::class, $res['rowName']); @@ -300,7 +366,9 @@ public function testDecodeValuesArray() $res = $this->mapper->decodeValues( $this->createField(ValueMapper::TYPE_ARRAY, 'arrayElementType', [ 'code' => ValueMapper::TYPE_STRING - ]), $this->createRow(['foo', 'bar']) + ]), + $this->createRow(['foo', 'bar']), + Result::RETURN_ASSOCIATIVE ); $this->assertEquals('foo', $res['rowName'][0]); @@ -337,7 +405,8 @@ public function testDecodeValuesStruct() $res = $this->mapper->decodeValues( [$field], - [$row] + [$row], + Result::RETURN_ASSOCIATIVE ); $this->assertEquals('Hello World', $res['structTest'][0]['rowName']); @@ -360,7 +429,7 @@ public function testDecodeValuesAnonymousField() $row = ['1337', 'John']; - $res = $this->mapper->decodeValues($fields, $row); + $res = $this->mapper->decodeValues($fields, $row, Result::RETURN_ASSOCIATIVE); $this->assertEquals('1337', $res['ID']); $this->assertEquals('John', $res[1]); diff --git a/tests/unit/fixtures/spanner/streaming-read-acceptance-test.json b/tests/unit/fixtures/spanner/streaming-read-acceptance-test.json index 718e782c9839..38b549b5cb9f 100644 --- a/tests/unit/fixtures/spanner/streaming-read-acceptance-test.json +++ b/tests/unit/fixtures/spanner/streaming-read-acceptance-test.json @@ -366,6 +366,82 @@ "{\n \"values\": [\"f\"]\n}" ], "name": "Multiple Row Chunks/Non Chunks Interleaved" + }, + { + "result": { + "value": [ + [ + "a" + ], + [ + "b" + ], + [ + "c" + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n },\n \"values\": [\"a\"]\n}", + "{\n \"values\": [\"b\"]\n}", + "{\n \"values\": [\"c\"],\n \"resumeToken\": \"theToken\"\n}" + ], + "name": "Buffers up to resume token" + }, + { + "result": { + "value": [ + [ + "a" + ], + [ + "b" + ], + [ + "c" + ], + [ + "d" + ], + [ + "e" + ], + [ + "f" + ], + [ + "g" + ], + [ + "h" + ], + [ + "i" + ] + ] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n },\n \"values\": [\"a\"]\n}", + "{\n \"values\": [\"b\"]\n}", + "{\n \"values\": [\"c\"]\n}", + "{\n \"values\": [\"d\"]\n}", + "{\n \"values\": [\"e\"]\n}", + "{\n \"values\": [\"f\"]\n}", + "{\n \"values\": [\"g\"]\n}", + "{\n \"values\": [\"h\"]\n}", + "{\n \"values\": [\"i\"]\n}", + "{\n \"values\": [\"j\"],\n \"chunkedValue\": true\n}" + ], + "name": "Buffers up to buffer limit" + }, + { + "result": { + "value": [] + }, + "chunks": [ + "{\n \"metadata\": {\n \"rowType\": {\n \"fields\": [{\n \"name\": \"f1\",\n \"type\": {\n \"code\": 6\n }\n }]\n }\n }}" + ], + "name": "Returns early with no values" } ] } From 9becfc7e0ab4d4408f1bd4bf297b966a1c212c3b Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Tue, 16 May 2017 10:43:02 -0400 Subject: [PATCH 11/46] Update README and Spanner composer.json --- README.md | 38 ++++++++++++++++++++++ src/Spanner/composer.json | 8 +++-- tests/snippets/Spanner/TransactionTest.php | 13 +++++++- tests/system/Spanner/ReadTest.php | 9 +---- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8fc1b37ab204..419a754033b5 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This client supports the following Google Cloud Platform services at a [Beta](#v This client supports the following Google Cloud Platform services at an [Alpha](#versioning) quality level: * [Google Cloud Pub/Sub](#google-cloud-pubsub-alpha) (Alpha) +* [Cloud Spanner](#cloud-spanner-alpha) (Alpha) * [Google Cloud Speech](#google-cloud-speech-alpha) (Alpha) * [Google Stackdriver Trace](#google-stackdriver-trace-alpha) (Alpha) @@ -390,6 +391,43 @@ Google Cloud Pub/Sub can be installed separately by requiring the `google/cloud- $ require google/cloud-pubsub ``` +## Cloud Spanner (Alpha) + +- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/spanner/spannerclient) +- [Official Documentation](https://cloud.google.com/spanner/docs) + +#### Preview + +```php +require 'vendor/autoload.php'; + +use Google\Cloud\Spanner\SpannerClient; + +$spanner = new SpannerClient([ + 'projectId' => 'my_project' +]); + +$db = $spanner->connect('my-instance', 'my-database'); + +$userQuery = $db->execute('SELECT * FROM Users WHERE id = @id', [ + 'parameters' => [ + 'id' => $userId + ] +]); + +$user = $userQuery->rows()->current(); + +echo 'Hello ' . $user['firstName']; +``` + +#### google/cloud-spanner + +Cloud Spanner can be installed separately by requiring the `google/cloud-spanner` composer package: + +``` +$ require google/cloud-spanner +``` + ## Google Cloud Speech (Alpha) - [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/speech/speechclient) diff --git a/src/Spanner/composer.json b/src/Spanner/composer.json index c71551886e92..dbe67efd14e5 100644 --- a/src/Spanner/composer.json +++ b/src/Spanner/composer.json @@ -4,11 +4,13 @@ "license": "Apache-2.0", "minimum-stability": "stable", "require": { - "google/cloud-core": "*" + "ext-grpc": "*", + "google/cloud-core": "^1.0", + "google/gax": "^0.8", + "google/proto-client-php": "^0.8" }, "suggest": { - "google/gax": "Required to support gRPC", - "google/proto-client-php": "Required to support gRPC" + "symfony/lock": "Required for the default session handler. Should be included as follows: symfony/lock:dev-master#1ba6ac9" }, "extra": { "component": { diff --git a/tests/snippets/Spanner/TransactionTest.php b/tests/snippets/Spanner/TransactionTest.php index 2c6596ef9e08..7514fd2e490f 100644 --- a/tests/snippets/Spanner/TransactionTest.php +++ b/tests/snippets/Spanner/TransactionTest.php @@ -49,7 +49,7 @@ public function setUp() $operation->reveal(), $session->reveal(), self::TRANSACTION - ], ['operation']); + ], ['operation', 'isRetry']); } private function stubOperation($stub = null) @@ -296,6 +296,17 @@ public function testState() $this->assertEquals(Transaction::STATE_ACTIVE, $res->returnVal()); } + public function testIsRetry() + { + $snippet = $this->snippetFromMethod(Transaction::class, 'isRetry'); + $snippet->addLocal('transaction', $this->transaction); + + $this->transaction->___setProperty('isRetry', true); + + $res = $snippet->invoke(); + $this->assertEquals('This is a retry transaction!', $res->output()); + } + private function resultGenerator() { yield [ diff --git a/tests/system/Spanner/ReadTest.php b/tests/system/Spanner/ReadTest.php index ed6eb1e025ce..992489cf3bef 100644 --- a/tests/system/Spanner/ReadTest.php +++ b/tests/system/Spanner/ReadTest.php @@ -82,14 +82,6 @@ public function testReadPoint() } } - // public function rangeProvider() - // { - // return [ - // // single key, open-open - - // ]; - // } - /** * covers 8 */ @@ -353,6 +345,7 @@ public function testBindBytesParameter() $row = $res->rows()->current(); $this->assertInstanceOf(Bytes::class, $row['foo']); $this->assertEquals($str, base64_decode($bytes->formatAsString())); + $this->assertEquals($str, (string)$bytes->get()); } /** From b78a47e5fd4b7325cf22d82767fb6498137a5157 Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Tue, 16 May 2017 18:34:20 -0400 Subject: [PATCH 12/46] update verbiage --- src/Spanner/Session/CacheSessionPool.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Spanner/Session/CacheSessionPool.php b/src/Spanner/Session/CacheSessionPool.php index 56e369955506..0149952cca62 100644 --- a/src/Spanner/Session/CacheSessionPool.php +++ b/src/Spanner/Session/CacheSessionPool.php @@ -47,7 +47,9 @@ * on an interval that matches when you expect to see a decrease in traffic. * This will help ensure you never run into issues where the Spanner backend is * locked up after having met the maximum number of sessions assigned per node - * (The current maximum sessions per node is 10k). + * For reference, the current maximum sessions per database per node is 10k. For + * more information on limits please see + * [here](https://cloud.google.com/spanner/docs/limits). * * Additionally, when expecting a long period of inactivity (such as a * maintenance window), please make sure to call From 57849d37c0d03de0d65f3f2648c5b64edd2b52fd Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Wed, 17 May 2017 14:01:57 -0400 Subject: [PATCH 13/46] small tweaks --- src/Spanner/Session/CacheSessionPool.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Spanner/Session/CacheSessionPool.php b/src/Spanner/Session/CacheSessionPool.php index 0149952cca62..229d299404b9 100644 --- a/src/Spanner/Session/CacheSessionPool.php +++ b/src/Spanner/Session/CacheSessionPool.php @@ -46,7 +46,7 @@ * the {@see Google\Cloud\Spanner\Session\CacheSessionPool::downsize()} method * on an interval that matches when you expect to see a decrease in traffic. * This will help ensure you never run into issues where the Spanner backend is - * locked up after having met the maximum number of sessions assigned per node + * locked up after having met the maximum number of sessions assigned per node. * For reference, the current maximum sessions per database per node is 10k. For * more information on limits please see * [here](https://cloud.google.com/spanner/docs/limits). @@ -176,7 +176,7 @@ public function acquire($context = SessionPoolInterface::CONTEXT_READ) list($session, $toCreate) = $this->config['lock']->synchronize(function () { $toCreate = []; $session = null; - $shouldSave = true; + $shouldSave = false; $item = $this->cacheItemPool->getItem($this->cacheKey); $data = (array) $item->get() ?: $this->initialize(); @@ -186,10 +186,12 @@ public function acquire($context = SessionPoolInterface::CONTEXT_READ) // for more. if ($data['queue']) { $session = $this->getSession($data); + $shouldSave = true; } elseif ($this->config['maxSessions'] <= $this->getSessionCount($data)) { $this->purgeOrphanedInUseSessions($data); $this->purgeOrphanedToCreateItems($data); $session = $this->getSession($data); + $shouldSave = true; } if (!$session) { @@ -198,8 +200,7 @@ public function acquire($context = SessionPoolInterface::CONTEXT_READ) if ($count < $this->config['maxSessions']) { $toCreate = $this->buildToCreateList(1); $data['toCreate'] += $toCreate; - } else { - $shouldSave = false; + $shouldSave = true; } } @@ -541,7 +542,7 @@ private function buildToCreateList($number) $time = $this->time(); for ($i = 0; $i < $number; $i++) { - $toCreate[uniqid($time . '_')] = $time; + $toCreate[uniqid($time . '_', true)] = $time; } return $toCreate; From 22b994d85976054585fc48ea8b2e73dfe3a1908b Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 17 May 2017 16:25:40 -0400 Subject: [PATCH 14/46] Add resource-prefix header to outgoing spanner requests --- composer.json | 2 +- src/Spanner/Connection/Grpc.php | 130 ++++++++++++++------- src/Spanner/Database.php | 15 +++ src/Spanner/Instance.php | 3 +- src/Spanner/Operation.php | 13 ++- src/Spanner/Session/Session.php | 6 +- src/Spanner/composer.json | 2 +- tests/unit/Spanner/Connection/GrpcTest.php | 100 ++++++++++------ tests/unit/Spanner/OperationTest.php | 2 + 9 files changed, 184 insertions(+), 89 deletions(-) diff --git a/composer.json b/composer.json index 4d56dddc8efe..81858f65719a 100644 --- a/composer.json +++ b/composer.json @@ -57,7 +57,7 @@ "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", "google/proto-client-php": "^0.12", - "google/gax": "^0.8", + "google/gax": "^0.9", "symfony/lock": "dev-master#1ba6ac9" }, "suggest": { diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index d5a5f9b9ca22..738d57e7b379 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -144,9 +144,10 @@ public function __construct(array $config = []) */ public function listInstanceConfigs(array $args) { + $projectId = $this->pluck('projectId', $args); return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ - $this->pluck('projectId', $args), - $args + $projectId, + $this->addResourcePrefixHeader($args, $projectId) ]); } @@ -155,9 +156,10 @@ public function listInstanceConfigs(array $args) */ public function getInstanceConfig(array $args) { + $projectId = $this->pluck('projectId', $args); return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ $this->pluck('name', $args), - $args + $this->addResourcePrefixHeader($args, $projectId) ]); } @@ -166,9 +168,10 @@ public function getInstanceConfig(array $args) */ public function listInstances(array $args) { + $projectId = $this->pluck('projectId', $args); return $this->send([$this->instanceAdminClient, 'listInstances'], [ - $this->pluck('projectId', $args), - $args + $projectId, + $this->addResourcePrefixHeader($args, $projectId) ]); } @@ -177,9 +180,10 @@ public function listInstances(array $args) */ public function getInstance(array $args) { + $projectId = $this->pluck('projectId', $args); return $this->send([$this->instanceAdminClient, 'getInstance'], [ $this->pluck('name', $args), - $args + $this->addResourcePrefixHeader($args, $projectId) ]); } @@ -188,12 +192,14 @@ public function getInstance(array $args) */ public function createInstance(array $args) { + $instanceName = $args['name']; + $instance = $this->instanceObject($args, true); $res = $this->send([$this->instanceAdminClient, 'createInstance'], [ $this->pluck('projectId', $args), $this->pluck('instanceId', $args), $instance, - $args + $this->addResourcePrefixHeader($args, $instanceName) ]); return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); @@ -204,6 +210,7 @@ public function createInstance(array $args) */ public function updateInstance(array $args) { + $instanceName = $args['name']; $instanceObject = $this->instanceObject($args); $mask = array_keys($instanceObject->serialize(new PhpArray([], false))); @@ -213,7 +220,7 @@ public function updateInstance(array $args) $res = $this->send([$this->instanceAdminClient, 'updateInstance'], [ $instanceObject, $fieldMask, - $args + $this->addResourcePrefixHeader($args, $instanceName) ]); return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); @@ -224,9 +231,10 @@ public function updateInstance(array $args) */ public function deleteInstance(array $args) { + $instanceName = $this->pluck('name', $args); return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ - $this->pluck('name', $args), - $args + $instanceName, + $this->addResourcePrefixHeader($args, $instanceName) ]); } @@ -235,9 +243,10 @@ public function deleteInstance(array $args) */ public function getInstanceIamPolicy(array $args) { + $resource = $this->pluck('resource', $args); return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ - $this->pluck('resource', $args), - $args + $resource, + $this->addResourcePrefixHeader($args, $resource) ]); } @@ -246,10 +255,11 @@ public function getInstanceIamPolicy(array $args) */ public function setInstanceIamPolicy(array $args) { + $resource = $this->pluck('resource', $args); return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ - $this->pluck('resource', $args), + $resource, $this->pluck('policy', $args), - $args + $this->addResourcePrefixHeader($args, $resource) ]); } @@ -258,10 +268,11 @@ public function setInstanceIamPolicy(array $args) */ public function testInstanceIamPermissions(array $args) { + $resource = $this->pluck('resource', $args); return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ - $this->pluck('resource', $args), + $resource, $this->pluck('permissions', $args), - $args + $this->addResourcePrefixHeader($args, $resource) ]); } @@ -270,9 +281,10 @@ public function testInstanceIamPermissions(array $args) */ public function listDatabases(array $args) { + $instanceName = $this->pluck('instance', $args); return $this->send([$this->databaseAdminClient, 'listDatabases'], [ - $this->pluck('instance', $args), - $args + $instanceName, + $this->addResourcePrefixHeader($args, $instanceName) ]); } @@ -281,11 +293,12 @@ public function listDatabases(array $args) */ public function createDatabase(array $args) { + $instanceName = $this->pluck('instance', $args); $res = $this->send([$this->databaseAdminClient, 'createDatabase'], [ - $this->pluck('instance', $args), + $instanceName, $this->pluck('createStatement', $args), $this->pluck('extraStatements', $args), - $args + $this->addResourcePrefixHeader($args, $instanceName) ]); return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); @@ -296,10 +309,11 @@ public function createDatabase(array $args) */ public function updateDatabaseDdl(array $args) { + $databaseName = $this->pluck('name', $args); $res = $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [ - $this->pluck('name', $args), + $databaseName, $this->pluck('statements', $args), - $args + $this->addResourcePrefixHeader($args, $databaseName) ]); return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); @@ -310,9 +324,10 @@ public function updateDatabaseDdl(array $args) */ public function dropDatabase(array $args) { + $databaseName = $this->pluck('name', $args); return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ - $this->pluck('name', $args), - $args + $databaseName, + $this->addResourcePrefixHeader($args, $databaseName) ]); } @@ -321,9 +336,10 @@ public function dropDatabase(array $args) */ public function getDatabase(array $args) { + $databaseName = $this->pluck('name', $args); return $this->send([$this->databaseAdminClient, 'getDatabase'], [ - $this->pluck('name', $args), - $args + $databaseName, + $this->addResourcePrefixHeader($args, $databaseName) ]); } @@ -332,9 +348,10 @@ public function getDatabase(array $args) */ public function getDatabaseDDL(array $args) { + $databaseName = $this->pluck('name', $args); return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ - $this->pluck('name', $args), - $args + $databaseName, + $this->addResourcePrefixHeader($args, $databaseName) ]); } @@ -343,9 +360,10 @@ public function getDatabaseDDL(array $args) */ public function getDatabaseIamPolicy(array $args) { + $databaseName = $this->pluck('resource', $args); return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ - $this->pluck('resource', $args), - $args + $databaseName, + $this->addResourcePrefixHeader($args, $databaseName) ]); } @@ -354,10 +372,11 @@ public function getDatabaseIamPolicy(array $args) */ public function setDatabaseIamPolicy(array $args) { + $databaseName = $this->pluck('resource', $args); return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ - $this->pluck('resource', $args), + $databaseName, $this->pluck('policy', $args), - $args + $this->addResourcePrefixHeader($args, $databaseName) ]); } @@ -366,10 +385,11 @@ public function setDatabaseIamPolicy(array $args) */ public function testDatabaseIamPermissions(array $args) { + $databaseName = $this->pluck('resource', $args); return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ - $this->pluck('resource', $args), + $databaseName, $this->pluck('permissions', $args), - $args + $this->addResourcePrefixHeader($args, $databaseName) ]); } @@ -378,9 +398,10 @@ public function testDatabaseIamPermissions(array $args) */ public function createSession(array $args) { + $databaseName = $this->pluck('database', $args); return $this->send([$this->spannerClient, 'createSession'], [ - $this->pluck('database', $args), - $args + $databaseName, + $this->addResourcePrefixHeader($args, $databaseName) ]); } @@ -389,9 +410,10 @@ public function createSession(array $args) */ public function getSession(array $args) { + $database = $this->pluck('database', $args); return $this->send([$this->spannerClient, 'getSession'], [ $this->pluck('name', $args), - $args + $this->addResourcePrefixHeader($args, $database) ]); } @@ -400,9 +422,10 @@ public function getSession(array $args) */ public function deleteSession(array $args) { + $database = $this->pluck('database', $args); return $this->send([$this->spannerClient, 'deleteSession'], [ $this->pluck('name', $args), - $args + $this->addResourcePrefixHeader($args, $database) ]); } @@ -438,10 +461,11 @@ public function executeStreamingSql(array $args) $args['transaction'] = $this->createTransactionSelector($args); + $database = $this->pluck('database', $args); return $this->send([$this->spannerClient, 'executeStreamingSql'], [ $this->pluck('session', $args), $this->pluck('sql', $args), - $args + $this->addResourcePrefixHeader($args, $database) ]); } @@ -457,12 +481,13 @@ public function streamingRead(array $args) $args['transaction'] = $this->createTransactionSelector($args); + $database = $this->pluck('database', $args); return $this->send([$this->spannerClient, 'streamingRead'], [ $this->pluck('session', $args), $this->pluck('table', $args), $this->pluck('columns', $args), $keySet, - $args + $this->addResourcePrefixHeader($args, $database) ]); } @@ -484,10 +509,11 @@ public function beginTransaction(array $args) $options->setReadWrite($readWrite); } + $database = $this->pluck('database', $args); return $this->send([$this->spannerClient, 'beginTransaction'], [ $this->pluck('session', $args), $options, - $args + $this->addResourcePrefixHeader($args, $database) ]); } @@ -547,10 +573,11 @@ public function commit(array $args) $args['singleUseTransaction'] = $options; } + $database = $this->pluck('database', $args); return $this->send([$this->spannerClient, 'commit'], [ $this->pluck('session', $args), $mutations, - $args + $this->addResourcePrefixHeader($args, $database) ]); } @@ -559,10 +586,11 @@ public function commit(array $args) */ public function rollback(array $args) { + $database = $this->pluck('database', $args); return $this->send([$this->spannerClient, 'rollback'], [ $this->pluck('session', $args), $this->pluck('transactionId', $args), - $args + $this->addResourcePrefixHeader($args, $database) ]); } @@ -766,4 +794,20 @@ private function formatTransactionOptions(array $transactionOptions) return $transactionOptions; } + + /** + * Add the `google-cloud-resource-prefix` header value to the request. + * + * @param array $args + * @param string $value + * @return array + */ + private function addResourcePrefixHeader(array $args, $value) + { + $args['userHeaders'] = [ + 'google-cloud-resource-prefix' => [$value] + ]; + + return $args; + } } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 331eaa22a30c..dae8ca7c088a 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -709,6 +709,11 @@ public function runTransaction(callable $operation, array $options = []) * * Mutations are committed in a single-use transaction. * + * Since this method does not feature replay protection, it may attempt to + * apply mutations more than once; if the mutations are not idempotent, this + * may lead to a failure being reported when the mutation was previously + * applied. + * * Example: * ``` * $database->insert('Posts', [ @@ -737,6 +742,11 @@ public function insert($table, array $data, array $options = []) * * Mutations are committed in a single-use transaction. * + * Since this method does not feature replay protection, it may attempt to + * apply mutations more than once; if the mutations are not idempotent, this + * may lead to a failure being reported when the mutation was previously + * applied. + * * Example: * ``` * $database->insertBatch('Posts', [ @@ -998,6 +1008,11 @@ public function replaceBatch($table, array $dataSet, array $options = []) * * Mutations are committed in a single-use transaction. * + * Since this method does not feature replay protection, it may attempt to + * apply mutations more than once; if the mutations are not idempotent, this + * may lead to a failure being reported when the mutation was previously + * applied. + * * Example: * ``` * $keySet = new KeySet([ diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 2c9bfa5dfe03..49dddc6fe6f8 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -235,7 +235,8 @@ public function exists(array $options = []) public function reload(array $options = []) { $this->info = $this->connection->getInstance($options + [ - 'name' => $this->name + 'name' => $this->name, + 'projectId' => $this->projectId ]); return $this->info; diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 54ce03b2ca50..5f4a0b7768a1 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -127,7 +127,8 @@ public function commit(Session $session, array $mutations, array $options = []) $res = $this->connection->commit($this->arrayFilterRemoveNull([ 'mutations' => $mutations, - 'session' => $session->name() + 'session' => $session->name(), + 'database' => $session->info()['database'] ]) + $options); return $this->mapper->createTimestampWithNanos($res['commitTimestamp']); @@ -147,7 +148,8 @@ public function rollback(Session $session, $transactionId, array $options = []) { return $this->connection->rollback([ 'transactionId' => $transactionId, - 'session' => $session->name() + 'session' => $session->name(), + 'database' => $session->info()['database'] ] + $options); } @@ -180,7 +182,8 @@ public function execute(Session $session, $sql, array $options = []) return $this->connection->executeStreamingSql([ 'sql' => $sql, - 'session' => $session->name() + 'session' => $session->name(), + 'database' => $session->info()['database'] ] + $options); }; @@ -223,7 +226,8 @@ public function read(Session $session, $table, KeySet $keySet, array $columns, a 'table' => $table, 'session' => $session->name(), 'columns' => $columns, - 'keySet' => $this->flattenKeySet($keySet) + 'keySet' => $this->flattenKeySet($keySet), + 'database' => $session->info()['database'] ] + $options); }; @@ -355,6 +359,7 @@ private function beginTransaction(Session $session, array $options = []) return $this->connection->beginTransaction($options + [ 'session' => $session->name(), + 'database' => $session->info()['database'] ]); } diff --git a/src/Spanner/Session/Session.php b/src/Spanner/Session/Session.php index c1ae06042f4a..a1a5b380ea08 100644 --- a/src/Spanner/Session/Session.php +++ b/src/Spanner/Session/Session.php @@ -102,7 +102,8 @@ public function exists(array $options = []) { try { $this->connection->getSession($options + [ - 'name' => $this->name() + 'name' => $this->name(), + 'database' => $this->database ]); return true; @@ -120,7 +121,8 @@ public function exists(array $options = []) public function delete(array $options = []) { return $this->connection->deleteSession($options + [ - 'name' => $this->name() + 'name' => $this->name(), + 'database' => $this->database ]); } diff --git a/src/Spanner/composer.json b/src/Spanner/composer.json index dbe67efd14e5..5d92a476cb9e 100644 --- a/src/Spanner/composer.json +++ b/src/Spanner/composer.json @@ -6,7 +6,7 @@ "require": { "ext-grpc": "*", "google/cloud-core": "^1.0", - "google/gax": "^0.8", + "google/gax": "^0.9", "google/proto-client-php": "^0.8" }, "suggest": { diff --git a/tests/unit/Spanner/Connection/GrpcTest.php b/tests/unit/Spanner/Connection/GrpcTest.php index e158a7ebd702..c66298a018e3 100644 --- a/tests/unit/Spanner/Connection/GrpcTest.php +++ b/tests/unit/Spanner/Connection/GrpcTest.php @@ -171,145 +171,162 @@ public function methodProvider() [ 'listInstanceConfigs', ['projectId' => self::PROJECT], - [self::PROJECT, []] + [self::PROJECT, ['userHeaders' => ['google-cloud-resource-prefix' => [self::PROJECT]]]] ], [ 'getInstanceConfig', - ['name' => $configName], - [$configName, []] + ['name' => $configName, 'projectId' => self::PROJECT], + [$configName, ['userHeaders' => ['google-cloud-resource-prefix' => [self::PROJECT]]]] ], [ 'listInstances', ['projectId' => self::PROJECT], - [self::PROJECT, []] + [self::PROJECT, ['userHeaders' => ['google-cloud-resource-prefix' => [self::PROJECT]]]] ], [ 'getInstance', - ['name' => $instanceName], - [$instanceName, []] + ['name' => $instanceName, 'projectId' => self::PROJECT], + [$instanceName, ['userHeaders' => ['google-cloud-resource-prefix' => [self::PROJECT]]]] ], [ 'createInstance', ['projectId' => self::PROJECT, 'instanceId' => $instanceName] + $instanceArgs, - [self::PROJECT, $instanceName, $instance, []], + [self::PROJECT, $instanceName, $instance, ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]], $lro, null ], [ 'updateInstance', $instanceArgs, - [$instance, $fieldMask, []], + [$instance, $fieldMask, ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]], $lro, null ], [ 'deleteInstance', ['name' => $instanceName], - [$instanceName, []] + [$instanceName, ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]] ], [ 'setInstanceIamPolicy', ['resource' => $instanceName, 'policy' => $policy], - [$instanceName, $policy, []] + [$instanceName, $policy, ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]] ], [ 'getInstanceIamPolicy', ['resource' => $instanceName], - [$instanceName, []] + [$instanceName, ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]] ], [ 'testInstanceIamPermissions', ['resource' => $instanceName, 'permissions' => $permissions], - [$instanceName, $permissions, []] + [$instanceName, $permissions, ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]] ], [ 'listDatabases', ['instance' => $instanceName], - [$instanceName, []] + [$instanceName, ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]] ], [ 'createDatabase', ['instance' => $instanceName, 'createStatement' => $createStmt, 'extraStatements' => []], - [$instanceName, $createStmt, [], []], + [$instanceName, $createStmt, [], ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]], $lro, null ], [ 'updateDatabaseDdl', ['name' => $databaseName, 'statements' => []], - [$databaseName, [], []], + [$databaseName, [], ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]], $lro, null ], [ 'dropDatabase', ['name' => $databaseName], - [$databaseName, []] + [$databaseName, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'getDatabaseDDL', ['name' => $databaseName], - [$databaseName, []] + [$databaseName, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'setDatabaseIamPolicy', ['resource' => $databaseName, 'policy' => $policy], - [$databaseName, $policy, []] + [$databaseName, $policy, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'getDatabaseIamPolicy', ['resource' => $databaseName], - [$databaseName, []] + [$databaseName, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'testDatabaseIamPermissions', ['resource' => $databaseName, 'permissions' => $permissions], - [$databaseName, $permissions, []] + [$databaseName, $permissions, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'createSession', ['database' => $databaseName], - [$databaseName, []] + [$databaseName, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'getSession', - ['name' => $sessionName], - [$sessionName, []] + ['name' => $sessionName, 'database' => $databaseName], + [$sessionName, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'deleteSession', - ['name' => $sessionName], - [$sessionName, []] + ['name' => $sessionName, 'database' => $databaseName], + [$sessionName, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'executeStreamingSql', [ 'session' => $sessionName, 'sql' => $sql, - 'transactionId' => $transactionName + 'transactionId' => $transactionName, + 'database' => $databaseName ] + $mapped, [$sessionName, $sql, [ 'transaction' => $transactionSelector, 'params' => $expectedParams, - 'paramTypes' => $expectedParamTypes + 'paramTypes' => $expectedParamTypes, + 'userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]] ]] ], [ 'streamingRead', - ['keySet' => [], 'transactionId' => $transactionName, 'session' => $sessionName, 'table' => $tableName, 'columns' => $columns], - [$sessionName, $tableName, $columns, $keySet, ['transaction' => $transactionSelector]] + [ + 'keySet' => [], + 'transactionId' => $transactionName, + 'session' => $sessionName, + 'table' => $tableName, + 'columns' => $columns, + 'database' => $databaseName, + ], + [$sessionName, $tableName, $columns, $keySet, ['transaction' => $transactionSelector, 'userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], // test read write [ 'beginTransaction', - ['session' => $sessionName, 'transactionOptions' => $readWriteTransactionArgs], - [$sessionName, $readWriteTransactionOptions, []] + [ + 'session' => $sessionName, + 'transactionOptions' => $readWriteTransactionArgs, + 'database' => $databaseName + ], + [$sessionName, $readWriteTransactionOptions, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], // test read only [ 'beginTransaction', - ['session' => $sessionName, 'transactionOptions' => $readOnlyTransactionArgs], - [$sessionName, $readOnlyTransactionOptions, []] + [ + 'session' => $sessionName, + 'transactionOptions' => $readOnlyTransactionArgs, + 'database' => $databaseName + ], + [$sessionName, $readOnlyTransactionOptions, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], // test insert // [ @@ -320,13 +337,22 @@ public function methodProvider() // test single-use transaction [ 'commit', - ['session' => $sessionName, 'mutations' => [], 'singleUseTransaction' => true], - [$sessionName, [], ['singleUseTransaction' => $readWriteTransactionOptions]] + [ + 'session' => $sessionName, + 'mutations' => [], + 'singleUseTransaction' => true, + 'database' => $databaseName + ], + [$sessionName, [], ['singleUseTransaction' => $readWriteTransactionOptions, 'userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], [ 'rollback', - ['session' => $sessionName, 'transactionId' => $transactionName], - [$sessionName, $transactionName, []] + [ + 'session' => $sessionName, + 'transactionId' => $transactionName, + 'database' => $databaseName + ], + [$sessionName, $transactionName, ['userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], // ['getOperation'], // ['cancelOperation'], diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php index af4f1e8b1da6..aa4eae0a2ac0 100644 --- a/tests/unit/Spanner/OperationTest.php +++ b/tests/unit/Spanner/OperationTest.php @@ -37,6 +37,7 @@ class OperationTest extends \PHPUnit_Framework_TestCase { const SESSION = 'my-session-id'; const TRANSACTION = 'my-transaction-id'; + const DATABASE = 'my-database'; const TIMESTAMP = '2017-01-09T18:05:22.534799Z'; private $connection; @@ -54,6 +55,7 @@ public function setUp() $session = $this->prophesize(Session::class); $session->name()->willReturn(self::SESSION); + $session->info()->willReturn(['database' => self::DATABASE]); $this->session = $session->reveal(); } From 867517b3fb8c3c80d2fb7c7ad83e14e63e92f1cf Mon Sep 17 00:00:00 2001 From: michaelbausor Date: Thu, 18 May 2017 06:44:26 -0700 Subject: [PATCH 15/46] Add Video Intelligence GAPIC client (#492) * Add GAPIC only video intelligence * Add docs * Update namespace capitalization * Address PR comments * Fix * escaping * Update grpc install description --- README.md | 35 ++ composer.json | 2 +- docs/contents/cloud-videointelligence.json | 18 + docs/external-classes.json | 9 + docs/manifest.json | 8 + src/VideoIntelligence/LICENSE | 202 +++++++++ src/VideoIntelligence/README.md | 36 ++ src/VideoIntelligence/V1beta1/README.md | 5 + .../VideoIntelligenceServiceClient.php | 429 ++++++++++++++++++ ...eo_intelligence_service_client_config.json | 33 ++ src/VideoIntelligence/VERSION | 1 + src/VideoIntelligence/composer.json | 24 + 12 files changed, 801 insertions(+), 1 deletion(-) create mode 100644 docs/contents/cloud-videointelligence.json create mode 100644 src/VideoIntelligence/LICENSE create mode 100644 src/VideoIntelligence/README.md create mode 100644 src/VideoIntelligence/V1beta1/README.md create mode 100644 src/VideoIntelligence/V1beta1/VideoIntelligenceServiceClient.php create mode 100644 src/VideoIntelligence/V1beta1/resources/video_intelligence_service_client_config.json create mode 100644 src/VideoIntelligence/VERSION create mode 100644 src/VideoIntelligence/composer.json diff --git a/README.md b/README.md index 8fc1b37ab204..7b45d29d790f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ This client supports the following Google Cloud Platform services at a [Beta](#v This client supports the following Google Cloud Platform services at an [Alpha](#versioning) quality level: * [Google Cloud Pub/Sub](#google-cloud-pubsub-alpha) (Alpha) * [Google Cloud Speech](#google-cloud-speech-alpha) (Alpha) +* [Google Cloud Video Intelligence](#google-cloud-videointelligence-alpha) (Alpha) * [Google Stackdriver Trace](#google-stackdriver-trace-alpha) (Alpha) If you need support for other Google APIs, please check out the [Google APIs Client Library for PHP](https://github.com/google/google-api-php-client). @@ -445,6 +446,40 @@ $storage = new StorageClient([ ]); ``` +## Google Cloud Video Intelligence (Alpha) + +- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/videointelligence/readme) +- [Official Documentation](https://cloud.google.com/video-intelligence/docs) + +#### Preview + +```php +require __DIR__ . '/vendor/autoload.php'; + +use Google\Cloud\VideoIntelligence\V1beta1\VideoIntelligenceServiceClient; +use google\cloud\videointelligence\v1beta1\Feature; + +$client = new VideoIntelligenceServiceClient(); + +$inputUri = "gs://example-bucket/example-video.mp4"; +$features = [ + Feature::LABEL_DETECTION, +]; +$operationResponse = $client->annotateVideo($inputUri, $features); +$operationResponse->pollUntilComplete(); +if ($operationResponse->operationSucceeded()) { + $results = $operationResponse->getResult(); + foreach ($results->getAnnotationResultsList() as $result) { + foreach ($result->getLabelAnnotationsList() as $labelAnnotation) { + echo "Label: " . $labelAnnotation->getDescription() . "\n"; + } + } +} else { + $error = $operationResponse->getError(); + echo "error: " . $error->getMessage() . "\n"; +} +``` + ## Google Stackdriver Trace (Alpha) - [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/trace/traceclient) diff --git a/composer.json b/composer.json index 38de7429e585..7c312fbaef98 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "league/json-guard": "^0.3", "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", - "google/proto-client-php": "^0.12", + "google/proto-client-php": "^0.13", "google/gax": "^0.8" }, "suggest": { diff --git a/docs/contents/cloud-videointelligence.json b/docs/contents/cloud-videointelligence.json new file mode 100644 index 000000000000..a43d2c6aeb97 --- /dev/null +++ b/docs/contents/cloud-videointelligence.json @@ -0,0 +1,18 @@ +{ + "title": "Video Intelligence", + "pattern": "videointelligence\/\\w{1,}", + "services": [{ + "title": "Overview", + "type": "videointelligence/readme" + }, { + "title": "v1beta1", + "type": "videointelligence/v1beta1/readme", + "patterns": [ + "videointelligence/v1beta1/\\w{1,}" + ], + "nav": [{ + "title": "VideoIntelligenceServiceClient", + "type": "videointelligence/v1beta1/videointelligenceserviceclient" + }] + }] +} diff --git a/docs/external-classes.json b/docs/external-classes.json index 2eab6a7b0fc5..97d764de7904 100644 --- a/docs/external-classes.json +++ b/docs/external-classes.json @@ -85,5 +85,14 @@ }, { "name": "PushConfig", "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/pubsub/v1/pubsub.php#L1827" +}, { + "name": "google\\cloud\\videointelligence\\v1beta1\\", + "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/videointelligence/v1beta1" +}, { + "name": "VideoContext", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/videointelligence/v1beta1/video_intelligence.php#L379" +}, { + "name": "google\\cloud\\videointelligence\\v1beta1\\Feature", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/videointelligence/v1beta1/video_intelligence.php#8" }] diff --git a/docs/manifest.json b/docs/manifest.json index 811e7c8ef55c..b69caa2a35d5 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -163,6 +163,14 @@ "master" ] }, + { + "id": "cloud-videointelligence", + "name": "google/cloud-videointelligence", + "defaultService": "videointelligence/videointelligenceserviceclient", + "versions": [ + "master" + ] + }, { "id": "cloud-vision", "name": "google/cloud-vision", diff --git a/src/VideoIntelligence/LICENSE b/src/VideoIntelligence/LICENSE new file mode 100644 index 000000000000..8f71f43fee3f --- /dev/null +++ b/src/VideoIntelligence/LICENSE @@ -0,0 +1,202 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/src/VideoIntelligence/README.md b/src/VideoIntelligence/README.md new file mode 100644 index 000000000000..04570727b8b2 --- /dev/null +++ b/src/VideoIntelligence/README.md @@ -0,0 +1,36 @@ +# Google Cloud PHP Video Intelligence + +> Idiomatic PHP client for [Cloud Video Intelligence](https://cloud.google.com/video-intelligence/). + +* [Homepage](http://googlecloudplatform.github.io/google-cloud-php) +* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-videointelligence/latest/readme) + +**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any +support requests, bug reports, or development contributions should be directed to +that project. + +This client supports transport over gRPC. In order to enable gRPC support please make sure to install and enable +the gRPC extension through PECL: + +```sh +$ pecl install grpc +``` + +If you are using Video Intelligence through the umbrella package (google/cloud), +you will also need to install the following dependencies through composer: + +```sh +$ composer require google/gax && composer require google/proto-client-php +``` + +Please take care in installing the same version of these libraries that are +outlined in the project's composer.json require-dev keyword. + +NOTE: Support for gRPC is currently at an Alpha quality level, meaning it is still +a work in progress and is more likely to get backwards-incompatible updates. + +## Installation + +``` +$ composer require google/cloud-videointelligence +``` diff --git a/src/VideoIntelligence/V1beta1/README.md b/src/VideoIntelligence/V1beta1/README.md new file mode 100644 index 000000000000..45254ec9fc85 --- /dev/null +++ b/src/VideoIntelligence/V1beta1/README.md @@ -0,0 +1,5 @@ +# Cloud Video Intelligence + +Google Cloud Video Intelligence API makes videos searchable, and discoverable, by extracting metadata with an easy to use API. + +For more information, see [cloud.google.com](https://cloud.google.com/video-intelligence/). diff --git a/src/VideoIntelligence/V1beta1/VideoIntelligenceServiceClient.php b/src/VideoIntelligence/V1beta1/VideoIntelligenceServiceClient.php new file mode 100644 index 000000000000..a283ef134b70 --- /dev/null +++ b/src/VideoIntelligence/V1beta1/VideoIntelligenceServiceClient.php @@ -0,0 +1,429 @@ +annotateVideo($inputUri, $features); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $videoIntelligenceServiceClient->annotateVideo($inputUri, $features); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $videoIntelligenceServiceClient->resumeOperation($operationName, 'annotateVideo'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } + * } finally { + * $videoIntelligenceServiceClient->close(); + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class VideoIntelligenceServiceClient +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'videointelligence.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.0.5'; + + private $grpcCredentialsHelper; + private $videoIntelligenceServiceStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + private $operationsClient; + + private static function getLongRunningDescriptors() + { + return [ + 'annotateVideo' => [ + 'operationReturnType' => '\google\cloud\videointelligence\v1beta1\AnnotateVideoResponse', + 'metadataReturnType' => '\google\cloud\videointelligence\v1beta1\AnnotateVideoProgress', + ], + ]; + } + + private static function getGapicVersion() + { + if (file_exists(__DIR__.'/../VERSION')) { + return trim(file_get_contents(__DIR__.'/../VERSION')); + } elseif (class_exists('\Google\Cloud\ServiceBuilder')) { + return \Google\Cloud\ServiceBuilder::VERSION; + } else { + return; + } + } + + /** + * Return an OperationsClient object with the same endpoint as $this. + * + * @return \Google\GAX\LongRunning\OperationsClient + */ + public function getOperationsClient() + { + return $this->operationsClient; + } + + /** + * Resume an existing long running operation that was previously started + * by a long running API method. If $methodName is not provided, or does + * not match a long running API method, then the operation can still be + * resumed, but the OperationResponse object will not deserialize the + * final response. + * + * @param string $operationName The name of the long running operation + * @param string $methodName The name of the method used to start the operation + * + * @return \Google\GAX\OperationResponse + */ + public function resumeOperation($operationName, $methodName = null) + { + $lroDescriptors = self::getLongRunningDescriptors(); + if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) { + $options = $lroDescriptors[$methodName]; + } else { + $options = []; + } + $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options); + $operation->reload(); + + return $operation; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'videointelligence.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type \Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * \Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Video Intelligence API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type \Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + ], + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'libName' => null, + 'libVersion' => null, + ]; + $options = array_merge($defaultOptions, $options); + + if (array_key_exists('operationsClient', $options)) { + $this->operationsClient = $options['operationsClient']; + } else { + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + ]); + } + + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'annotateVideo' => $defaultDescriptors, + ]; + $longRunningDescriptors = self::getLongRunningDescriptors(); + foreach ($longRunningDescriptors as $method => $longRunningDescriptor) { + $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient]; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/video_intelligence_service_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.cloud.videointelligence.v1beta1.VideoIntelligenceService', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (array_key_exists('sslCreds', $options)) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createVideoIntelligenceServiceStubFunction = function ($hostname, $opts) { + return new VideoIntelligenceServiceGrpcClient($hostname, $opts); + }; + if (array_key_exists('createVideoIntelligenceServiceStubFunction', $options)) { + $createVideoIntelligenceServiceStubFunction = $options['createVideoIntelligenceServiceStubFunction']; + } + $this->videoIntelligenceServiceStub = $this->grpcCredentialsHelper->createStub( + $createVideoIntelligenceServiceStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Performs asynchronous video annotation. Progress and results can be + * retrieved through the `google.longrunning.Operations` interface. + * `Operation.metadata` contains `AnnotateVideoProgress` (progress). + * `Operation.response` contains `AnnotateVideoResponse` (results). + * + * Sample code: + * ``` + * try { + * $videoIntelligenceServiceClient = new VideoIntelligenceServiceClient(); + * $inputUri = ""; + * $features = []; + * $operationResponse = $videoIntelligenceServiceClient->annotateVideo($inputUri, $features); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $videoIntelligenceServiceClient->annotateVideo($inputUri, $features); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $videoIntelligenceServiceClient->resumeOperation($operationName, 'annotateVideo'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } + * } finally { + * $videoIntelligenceServiceClient->close(); + * } + * ``` + * + * @param string $inputUri Input video location. Currently, only + * [Google Cloud Storage](https://cloud.google.com/storage/) URIs are + * supported, which must be specified in the following format: + * `gs://bucket-id/object-id` (other URI formats return + * [google.rpc.Code.INVALID_ARGUMENT][google.rpc.Code.INVALID_ARGUMENT]). For more information, see + * [Request URIs](/storage/docs/reference-uris). + * A video URI may include wildcards in `object-id`, and thus identify + * multiple videos. Supported wildcards: '*' to match 0 or more characters; + * '?' to match 1 character. If unset, the input video should be embedded + * in the request as `input_content`. If set, `input_content` should be unset. + * @param int[] $features Requested video annotation features. For allowed values, use constants defined on + * {@see google\cloud\videointelligence\v1beta1\Feature}. + * @param array $optionalArgs { + * Optional. + * + * @type string $inputContent + * The video data bytes. Encoding: base64. If unset, the input video(s) + * should be specified via `input_uri`. If set, `input_uri` should be unset. + * @type VideoContext $videoContext + * Additional video context and/or feature-specific parameters. + * @type string $outputUri + * Optional location where the output (in JSON format) should be stored. + * Currently, only [Google Cloud Storage](https://cloud.google.com/storage/) + * URIs are supported, which must be specified in the following format: + * `gs://bucket-id/object-id` (other URI formats return + * [google.rpc.Code.INVALID_ARGUMENT][google.rpc.Code.INVALID_ARGUMENT]). For more information, see + * [Request URIs](/storage/docs/reference-uris). + * @type string $locationId + * Optional cloud region where annotation should take place. Supported cloud + * regions: `us-east1`, `us-west1`, `europe-west1`, `asia-east1`. If no region + * is specified, a region will be determined based on video file location. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\longrunning\Operation + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function annotateVideo($inputUri, $features, $optionalArgs = []) + { + $request = new AnnotateVideoRequest(); + $request->setInputUri($inputUri); + foreach ($features as $elem) { + $request->addFeatures($elem); + } + if (isset($optionalArgs['inputContent'])) { + $request->setInputContent($optionalArgs['inputContent']); + } + if (isset($optionalArgs['videoContext'])) { + $request->setVideoContext($optionalArgs['videoContext']); + } + if (isset($optionalArgs['outputUri'])) { + $request->setOutputUri($optionalArgs['outputUri']); + } + if (isset($optionalArgs['locationId'])) { + $request->setLocationId($optionalArgs['locationId']); + } + + $mergedSettings = $this->defaultCallSettings['annotateVideo']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->videoIntelligenceServiceStub, + 'AnnotateVideo', + $mergedSettings, + $this->descriptors['annotateVideo'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->videoIntelligenceServiceStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/VideoIntelligence/V1beta1/resources/video_intelligence_service_client_config.json b/src/VideoIntelligence/V1beta1/resources/video_intelligence_service_client_config.json new file mode 100644 index 000000000000..7dd61bbb7b5d --- /dev/null +++ b/src/VideoIntelligence/V1beta1/resources/video_intelligence_service_client_config.json @@ -0,0 +1,33 @@ +{ + "interfaces": { + "google.cloud.videointelligence.v1beta1.VideoIntelligenceService": { + "retry_codes": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 1000, + "retry_delay_multiplier": 2.5, + "max_retry_delay_millis": 120000, + "initial_rpc_timeout_millis": 120000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 120000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "AnnotateVideo": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/VideoIntelligence/VERSION b/src/VideoIntelligence/VERSION new file mode 100644 index 000000000000..6e8bf73aa550 --- /dev/null +++ b/src/VideoIntelligence/VERSION @@ -0,0 +1 @@ +0.1.0 diff --git a/src/VideoIntelligence/composer.json b/src/VideoIntelligence/composer.json new file mode 100644 index 000000000000..b2730781b126 --- /dev/null +++ b/src/VideoIntelligence/composer.json @@ -0,0 +1,24 @@ +{ + "name": "google/cloud-videointelligence", + "description": "Cloud Video Intelligence Client for PHP", + "license": "Apache-2.0", + "minimum-stability": "stable", + "require": { + "ext-grpc": "*", + "google/proto-client-php": "^0.13", + "google/gax": "^0.9" + }, + "extra": { + "component": { + "id": "cloud-videointelligence", + "target": "GoogleCloudPlatform/google-cloud-php-videointelligence.git", + "path": "src/VideoIntelligence", + "entry": null + } + }, + "autoload": { + "psr-4": { + "Google\\Cloud\\VideoIntelligence\\": "" + } + } +} From 33f743a711c47d66c710a8449ec2a616d99399f3 Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Thu, 18 May 2017 11:07:42 -0400 Subject: [PATCH 16/46] inverse retry condition --- src/Spanner/Result.php | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index 25a9a7d7dfdb..e866d514affc 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -202,20 +202,22 @@ public function rows($format = self::RETURN_ASSOCIATIVE) $generator->next(); } catch (ServiceException $ex) { - if (!$shouldRetry || $ex->getCode() !== Grpc\STATUS_UNAVAILABLE) { - throw $ex; - } + if ($shouldRetry && $ex->getCode() === Grpc\STATUS_UNAVAILABLE) { + $backoff = new ExponentialBackoff($this->retries, function (ServiceException $ex) { + return $ex->getCode() === Grpc\STATUS_UNAVAILABLE + ? true + : false; + }); + + // Attempt to resume using our last stored resume token. If we + // successfully resume, flush the buffer. + $generator = $backoff->execute($call, [$this->resumeToken]); + $bufferedResults = []; - $backoff = new ExponentialBackoff($this->retries, function (ServiceException $ex) { - return $ex->getCode() === Grpc\STATUS_UNAVAILABLE - ? true - : false; - }); + continue; + } - // Attempt to resume using our last stored resume token. If we - // successfully resume, flush the buffer. - $generator = $backoff->execute($call, [$this->resumeToken]); - $bufferedResults = []; + throw $ex; } } From 49a598e2d518f0244d007cf68f812d4951126a38 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 18 May 2017 11:22:19 -0400 Subject: [PATCH 17/46] Prepare v0.29.0 (#493) --- docs/manifest.json | 7 +++++++ src/BigQuery/BigQueryClient.php | 2 +- src/BigQuery/VERSION | 2 +- src/Core/VERSION | 2 +- src/ErrorReporting/VERSION | 2 +- src/Logging/LoggingClient.php | 2 +- src/Logging/VERSION | 2 +- src/Monitoring/VERSION | 2 +- src/PubSub/PubSubClient.php | 2 +- src/PubSub/VERSION | 2 +- src/VideoIntelligence/VERSION | 2 +- 11 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index b69caa2a35d5..e83c64c22cc9 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -51,6 +51,7 @@ "name": "google/cloud-bigquery", "defaultService": "bigquery/bigqueryclient", "versions": [ + "v0.2.1", "v0.2.0", "v0.1.0", "master" @@ -61,6 +62,7 @@ "name": "google/cloud-core", "defaultService": "core/readme", "versions": [ + "v1.3.0", "v1.2.0", "v1.1.1", "v1.1.0", @@ -85,6 +87,7 @@ "name": "google/cloud-error-reporting", "defaultService": "errorreporting/readme", "versions": [ + "v0.2.0", "v0.1.0", "master" ] @@ -94,6 +97,7 @@ "name": "google/cloud-logging", "defaultService": "logging/loggingclient", "versions": [ + "v1.2.0", "v1.1.0", "v1.0.0", "v0.1.0", @@ -105,6 +109,7 @@ "name": "google/cloud-monitoring", "defaultService": "monitoring/readme", "versions": [ + "v0.2.0", "v0.1.0", "master" ] @@ -125,6 +130,7 @@ "name": "google/cloud-pubsub", "defaultService": "pubsub/pubsubclient", "versions": [ + "v0.5.0", "v0.4.0", "v0.3.0", "v0.2.0", @@ -168,6 +174,7 @@ "name": "google/cloud-videointelligence", "defaultService": "videointelligence/videointelligenceserviceclient", "versions": [ + "v0.1.0", "master" ] }, diff --git a/src/BigQuery/BigQueryClient.php b/src/BigQuery/BigQueryClient.php index 521297a80ae6..740729e344ee 100644 --- a/src/BigQuery/BigQueryClient.php +++ b/src/BigQuery/BigQueryClient.php @@ -45,7 +45,7 @@ class BigQueryClient use ClientTrait; use JobConfigurationTrait; - const VERSION = '0.2.0'; + const VERSION = '0.2.1'; const SCOPE = 'https://www.googleapis.com/auth/bigquery'; const INSERT_SCOPE = 'https://www.googleapis.com/auth/bigquery.insertdata'; diff --git a/src/BigQuery/VERSION b/src/BigQuery/VERSION index 341cf11faf9a..7dff5b892112 100644 --- a/src/BigQuery/VERSION +++ b/src/BigQuery/VERSION @@ -1 +1 @@ -0.2.0 \ No newline at end of file +0.2.1 \ No newline at end of file diff --git a/src/Core/VERSION b/src/Core/VERSION index 867e52437ab8..589268e6fedb 100644 --- a/src/Core/VERSION +++ b/src/Core/VERSION @@ -1 +1 @@ -1.2.0 \ No newline at end of file +1.3.0 \ No newline at end of file diff --git a/src/ErrorReporting/VERSION b/src/ErrorReporting/VERSION index 6c6aa7cb0918..341cf11faf9a 100644 --- a/src/ErrorReporting/VERSION +++ b/src/ErrorReporting/VERSION @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.2.0 \ No newline at end of file diff --git a/src/Logging/LoggingClient.php b/src/Logging/LoggingClient.php index 503e15122109..5c41891b3a38 100644 --- a/src/Logging/LoggingClient.php +++ b/src/Logging/LoggingClient.php @@ -67,7 +67,7 @@ class LoggingClient use ArrayTrait; use ClientTrait; - const VERSION = '1.1.0'; + const VERSION = '1.2.0'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/logging.admin'; const READ_ONLY_SCOPE = 'https://www.googleapis.com/auth/logging.read'; diff --git a/src/Logging/VERSION b/src/Logging/VERSION index 1cc5f657e054..867e52437ab8 100644 --- a/src/Logging/VERSION +++ b/src/Logging/VERSION @@ -1 +1 @@ -1.1.0 \ No newline at end of file +1.2.0 \ No newline at end of file diff --git a/src/Monitoring/VERSION b/src/Monitoring/VERSION index 6c6aa7cb0918..341cf11faf9a 100644 --- a/src/Monitoring/VERSION +++ b/src/Monitoring/VERSION @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.2.0 \ No newline at end of file diff --git a/src/PubSub/PubSubClient.php b/src/PubSub/PubSubClient.php index a78f5e161f48..588cab9a0df8 100644 --- a/src/PubSub/PubSubClient.php +++ b/src/PubSub/PubSubClient.php @@ -85,7 +85,7 @@ class PubSubClient use IncomingMessageTrait; use ResourceNameTrait; - const VERSION = '0.4.0'; + const VERSION = '0.5.0'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/pubsub'; diff --git a/src/PubSub/VERSION b/src/PubSub/VERSION index 60a2d3e96c80..79a2734bbf3d 100644 --- a/src/PubSub/VERSION +++ b/src/PubSub/VERSION @@ -1 +1 @@ -0.4.0 \ No newline at end of file +0.5.0 \ No newline at end of file diff --git a/src/VideoIntelligence/VERSION b/src/VideoIntelligence/VERSION index 6e8bf73aa550..6c6aa7cb0918 100644 --- a/src/VideoIntelligence/VERSION +++ b/src/VideoIntelligence/VERSION @@ -1 +1 @@ -0.1.0 +0.1.0 \ No newline at end of file From 6c9e9a2bd75fc5e676c7d1587a1bbf8aaf40889a Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 18 May 2017 11:22:36 -0400 Subject: [PATCH 18/46] Update docs to include new core types (#494) --- docs/contents/cloud-core.json | 6 ++++++ docs/contents/cloud-pubsub.json | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/contents/cloud-core.json b/docs/contents/cloud-core.json index 3b0b80bc4949..40dbe0463fd1 100644 --- a/docs/contents/cloud-core.json +++ b/docs/contents/cloud-core.json @@ -30,8 +30,14 @@ "title": "StreamableUploader", "type": "core/upload/streamableuploader" }] + }, { + "title": "Duration", + "type": "core/duration" }, { "title": "Int64", "type": "core/int64" + }, { + "title": "Timestamp", + "type": "core/timestamp" }] } diff --git a/docs/contents/cloud-pubsub.json b/docs/contents/cloud-pubsub.json index e2ed334e31c9..4994f565b8bb 100644 --- a/docs/contents/cloud-pubsub.json +++ b/docs/contents/cloud-pubsub.json @@ -16,12 +16,6 @@ }, { "title": "Topic", "type": "pubsub/topic" - }, { - "title": "Duration", - "type": "pubsub/duration" - }, { - "title": "Timestamp", - "type": "pubsub/timestamp" }, { "title": "v1", "type": "pubsub/v1/readme", From f78600b7e477dd582149e8c3bef0b4ede9e3dcee Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 18 May 2017 12:09:30 -0400 Subject: [PATCH 19/46] README updates / Add VI to list of includes (#495) * readme fixes * Add VI to list of includes --- README.md | 42 ++++++++++++++++----------------- docs/contents/google-cloud.json | 1 + 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7b45d29d790f..bbecbafbe7e4 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ This client supports the following Google Cloud Platform services at a [Beta](#v This client supports the following Google Cloud Platform services at an [Alpha](#versioning) quality level: * [Google Cloud Pub/Sub](#google-cloud-pubsub-alpha) (Alpha) * [Google Cloud Speech](#google-cloud-speech-alpha) (Alpha) -* [Google Cloud Video Intelligence](#google-cloud-videointelligence-alpha) (Alpha) +* [Google Cloud Video Intelligence](#google-cloud-video-intelligence-alpha) (Alpha) * [Google Stackdriver Trace](#google-stackdriver-trace-alpha) (Alpha) If you need support for other Google APIs, please check out the [Google APIs Client Library for PHP](https://github.com/google/google-api-php-client). @@ -426,26 +426,6 @@ Google Cloud Speech can be installed separately by requiring the `google/cloud-s $ require google/cloud-speech ``` -## Caching Access Tokens - -By default the library will use a simple in-memory caching implementation, however it is possible to override this behavior by passing a [PSR-6](http://www.php-fig.org/psr/psr-6/) caching implementation in to the desired client. - -The following example takes advantage of [Symfony's Cache Component](https://github.com/symfony/cache). - -```php -require 'vendor/autoload.php'; - -use Google\Cloud\Storage\StorageClient; -use Symfony\Component\Cache\Adapter\ArrayAdapter; - -// Please take the proper precautions when storing your access tokens in a cache no matter the implementation. -$cache = new ArrayAdapter(); - -$storage = new StorageClient([ - 'authCache' => $cache -]); -``` - ## Google Cloud Video Intelligence (Alpha) - [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/videointelligence/readme) @@ -513,6 +493,26 @@ foreach($traceClient->traces() as $trace) { } ``` +## Caching Access Tokens + +By default the library will use a simple in-memory caching implementation, however it is possible to override this behavior by passing a [PSR-6](http://www.php-fig.org/psr/psr-6/) caching implementation in to the desired client. + +The following example takes advantage of [Symfony's Cache Component](https://github.com/symfony/cache). + +```php +require 'vendor/autoload.php'; + +use Google\Cloud\Storage\StorageClient; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +// Please take the proper precautions when storing your access tokens in a cache no matter the implementation. +$cache = new ArrayAdapter(); + +$storage = new StorageClient([ + 'authCache' => $cache +]); +``` + ## Versioning This library follows [Semantic Versioning](http://semver.org/). diff --git a/docs/contents/google-cloud.json b/docs/contents/google-cloud.json index afa708f3cff4..5fc3b2f4adc5 100644 --- a/docs/contents/google-cloud.json +++ b/docs/contents/google-cloud.json @@ -15,6 +15,7 @@ "cloud-speech", "cloud-storage", "cloud-translate", + "cloud-videointelligence", "cloud-vision", "cloud-core" ] From 452e730baa61e4774b27896c1d7434fbc2d8b353 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 18 May 2017 12:09:39 -0400 Subject: [PATCH 20/46] increase ServiceBuilder version for v0.29.0 release (#496) --- docs/manifest.json | 1 + src/ServiceBuilder.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index e83c64c22cc9..9ab087c972d9 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -11,6 +11,7 @@ "name": "google/cloud", "defaultService": "servicebuilder", "versions": [ + "v0.29.0", "v0.28.0", "v0.27.0", "v0.26.0", diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index bcbed5c9f11d..42b694925592 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -49,7 +49,7 @@ */ class ServiceBuilder { - const VERSION = '0.28.0'; + const VERSION = '0.29.0'; /** * @var array Configuration options to be used between clients. From 71680ddc48c1360cc37cd8eeb53caad4fe37f0cf Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 18 May 2017 14:34:54 -0400 Subject: [PATCH 21/46] Prepare v0.30.0 (#497) * Prepare v0.30.0 * Add GAPIC to Spanner docs table of contents --- docs/contents/cloud-spanner.json | 24 +++++++++++++++++++++--- docs/contents/google-cloud.json | 1 + docs/manifest.json | 3 +++ src/Core/VERSION | 2 +- src/ServiceBuilder.php | 2 +- src/Spanner/SpannerClient.php | 2 +- src/Spanner/V1/README.md | 16 ++++++++++++++++ src/Spanner/VERSION | 1 + 8 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 src/Spanner/V1/README.md create mode 100644 src/Spanner/VERSION diff --git a/docs/contents/cloud-spanner.json b/docs/contents/cloud-spanner.json index 274b72c15e06..af768389970d 100644 --- a/docs/contents/cloud-spanner.json +++ b/docs/contents/cloud-spanner.json @@ -4,15 +4,15 @@ "services": [{ "title": "SpannerClient", "type": "spanner/spannerclient" - }, { - "title": "Configuration", - "type": "spanner/configuration" }, { "title": "Database", "type": "spanner/database" }, { "title": "Instance", "type": "spanner/instance" + }, { + "title": "InstanceConfiguration", + "type": "spanner/instanceconfiguration" }, { "title": "Snapshot", "type": "spanner/snapshot" @@ -43,5 +43,23 @@ },{ "title": "CacheSessionPool", "type": "spanner/session/cachesessionpool" + }, { + "title": "v1", + "type": "spanner/v1/readme", + "patterns": [ + "spanner/v1/\\w{1,}", + "spanner/admin/database/v1/\\w{1,}", + "spanner/admin/instance/v1/\\w{1,}" + ], + "nav": [{ + "title": "SpannerClient", + "type": "spanner/v1/spannerclient" + }, { + "title": "DatabaseAdminClient", + "type": "spanner/admin/database/v1/databaseadminclient" + }, { + "title": "InstanceAdminClient", + "type": "spanner/admin/instance/v1/instanceadminclient" + }] }] } diff --git a/docs/contents/google-cloud.json b/docs/contents/google-cloud.json index 5fc3b2f4adc5..c7a5ba8e8932 100644 --- a/docs/contents/google-cloud.json +++ b/docs/contents/google-cloud.json @@ -12,6 +12,7 @@ "cloud-monitoring", "cloud-language", "cloud-pubsub", + "cloud-spanner", "cloud-speech", "cloud-storage", "cloud-translate", diff --git a/docs/manifest.json b/docs/manifest.json index fd163dbb508b..dcae5012050e 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -11,6 +11,7 @@ "name": "google/cloud", "defaultService": "servicebuilder", "versions": [ + "v0.30.0", "v0.29.0", "v0.28.0", "v0.27.0", @@ -63,6 +64,7 @@ "name": "google/cloud-core", "defaultService": "core/readme", "versions": [ + "v1.4.0", "v1.3.0", "v1.2.0", "v1.1.1", @@ -144,6 +146,7 @@ "name": "google/cloud-spanner", "defaultService": "spanner/spannerclient", "versions": [ + "v0.1.0", "master" ] }, diff --git a/src/Core/VERSION b/src/Core/VERSION index 589268e6fedb..e21e727f96fa 100644 --- a/src/Core/VERSION +++ b/src/Core/VERSION @@ -1 +1 @@ -1.3.0 \ No newline at end of file +1.4.0 \ No newline at end of file diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index 0cf0b0e3151a..d2198b842a5b 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -50,7 +50,7 @@ */ class ServiceBuilder { - const VERSION = '0.29.0'; + const VERSION = '0.30.0'; /** * @var array Configuration options to be used between clients. diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 5b72de2a684a..878a0900eb5d 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -65,7 +65,7 @@ class SpannerClient use LROTrait; use ValidateTrait; - const VERSION = 'master'; + const VERSION = '0.1.0'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data'; const ADMIN_SCOPE = 'https://www.googleapis.com/auth/spanner.admin'; diff --git a/src/Spanner/V1/README.md b/src/Spanner/V1/README.md new file mode 100644 index 000000000000..de583242a7c3 --- /dev/null +++ b/src/Spanner/V1/README.md @@ -0,0 +1,16 @@ +# Google Cloud PHP Spanner + +> Idiomatic PHP client for [Cloud Spanner](https://cloud.google.com/spanner/). + +* [Homepage](http://googlecloudplatform.github.io/google-cloud-php) +* [API documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/cloud-spanner/latest/spanner/spannerclient) + +**NOTE:** This repository is part of [Google Cloud PHP](https://github.com/googlecloudplatform/google-cloud-php). Any +support requests, bug reports, or development contributions should be directed to +that project. + +## Installation + +``` +$ composer require google/cloud-spanner +``` diff --git a/src/Spanner/VERSION b/src/Spanner/VERSION new file mode 100644 index 000000000000..6c6aa7cb0918 --- /dev/null +++ b/src/Spanner/VERSION @@ -0,0 +1 @@ +0.1.0 \ No newline at end of file From 203e5c5a0c477c32f84c87c0473e7c5d83d86fd8 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 18 May 2017 17:25:13 -0400 Subject: [PATCH 22/46] make param type more specific (#500) --- src/Spanner/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index dae8ca7c088a..7205ca0bd6e0 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -283,7 +283,7 @@ public function exists(array $options = []) * @param array $options [optional] { * Configuration Options * - * @type array $statements Additional DDL statements. + * @type string[] $statements Additional DDL statements. * } * @return LongRunningOperation */ From c48e546e1eaf86a3c8e82e98dd65e0a6672145de Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 18 May 2017 14:25:22 -0700 Subject: [PATCH 23/46] fixes extraStatements bug for Spanner\Instance::createDatabase (#499) * fixes extraStatements bug for Spanner\Instance::createDatabase * fixes tests --- src/Spanner/Connection/Grpc.php | 1 - tests/unit/Spanner/Connection/GrpcTest.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 738d57e7b379..9130b68c9f63 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -297,7 +297,6 @@ public function createDatabase(array $args) $res = $this->send([$this->databaseAdminClient, 'createDatabase'], [ $instanceName, $this->pluck('createStatement', $args), - $this->pluck('extraStatements', $args), $this->addResourcePrefixHeader($args, $instanceName) ]); diff --git a/tests/unit/Spanner/Connection/GrpcTest.php b/tests/unit/Spanner/Connection/GrpcTest.php index c66298a018e3..2de216822c03 100644 --- a/tests/unit/Spanner/Connection/GrpcTest.php +++ b/tests/unit/Spanner/Connection/GrpcTest.php @@ -230,7 +230,7 @@ public function methodProvider() [ 'createDatabase', ['instance' => $instanceName, 'createStatement' => $createStmt, 'extraStatements' => []], - [$instanceName, $createStmt, [], ['userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]], + [$instanceName, $createStmt, ['extraStatements' => [], 'userHeaders' => ['google-cloud-resource-prefix' => [$instanceName]]]], $lro, null ], From 0632d3a71d0cb2b43cf2d649cd4c771a4d78191e Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 18 May 2017 20:28:58 -0700 Subject: [PATCH 24/46] corrects spanner dependency on core and removes symfony/lock requirement (#503) * corrects spanner dependency on core and removes symfony/lock requirement * changes core php req to >=5.5 --- src/Core/composer.json | 8 +++++--- src/Spanner/composer.json | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Core/composer.json b/src/Core/composer.json index 2d27262dc7c1..6072c3671b46 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -4,14 +4,16 @@ "license": "Apache-2.0", "minimum-stability": "stable", "require": { - "php": ">=5.5.9", + "php": ">=5.5", "rize/uri-template": "~0.3", "google/auth": "^0.11", "guzzlehttp/guzzle": "^5.3|^6.0", "guzzlehttp/psr7": "^1.2", "monolog/monolog": "~1", - "psr/http-message": "1.0.*", - "symfony/lock": "dev-master" + "psr/http-message": "1.0.*" + }, + "suggest": { + "symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: dev-master#1ba6ac9" }, "extra": { "component": { diff --git a/src/Spanner/composer.json b/src/Spanner/composer.json index 5d92a476cb9e..64d1b341b7e3 100644 --- a/src/Spanner/composer.json +++ b/src/Spanner/composer.json @@ -5,7 +5,7 @@ "minimum-stability": "stable", "require": { "ext-grpc": "*", - "google/cloud-core": "^1.0", + "google/cloud-core": "^1.4", "google/gax": "^0.9", "google/proto-client-php": "^0.8" }, From f9fad9a9d2766d09a976d44f4ab2f734fe1528e6 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Thu, 18 May 2017 20:31:52 -0700 Subject: [PATCH 25/46] Cast keySet keys to arrays instead of wrapping them (#505) * cast keySet keys to arrays instead of wrapping them * adds tests for keyset formatting --- src/Spanner/Connection/Grpc.php | 2 +- tests/unit/Spanner/Connection/GrpcTest.php | 28 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 9130b68c9f63..22c29eb89e3c 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -661,7 +661,7 @@ private function formatKeySet(array $keySet) $keySet['keys'] = []; foreach ($keys as $key) { - $keySet['keys'][] = $this->formatListForApi([$key]); + $keySet['keys'][] = $this->formatListForApi((array) $key); } } diff --git a/tests/unit/Spanner/Connection/GrpcTest.php b/tests/unit/Spanner/Connection/GrpcTest.php index 2de216822c03..2ec08bb6de0f 100644 --- a/tests/unit/Spanner/Connection/GrpcTest.php +++ b/tests/unit/Spanner/Connection/GrpcTest.php @@ -124,6 +124,10 @@ public function methodProvider() $keySetArgs = []; $keySet = (new KeySet) ->deserialize($keySetArgs, $codec); + $keySetSingular = (new KeySet()) + ->deserialize(['keys' => [['values' => ['number_value' => 1]]]], $codec); + $keySetComposite = (new KeySet()) + ->deserialize(['keys' => [['values' => [['number_value' => 1], ['number_value' => 1]]]]], $codec); $readWriteTransactionArgs = ['readWrite' => []]; $readWriteTransactionOptions = new TransactionOptions; @@ -308,6 +312,30 @@ public function methodProvider() ], [$sessionName, $tableName, $columns, $keySet, ['transaction' => $transactionSelector, 'userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] ], + [ + 'streamingRead', + [ + 'keySet' => ['keys' => [1]], + 'transactionId' => $transactionName, + 'session' => $sessionName, + 'table' => $tableName, + 'columns' => $columns, + 'database' => $databaseName, + ], + [$sessionName, $tableName, $columns, $keySetSingular, ['transaction' => $transactionSelector, 'userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] + ], + [ + 'streamingRead', + [ + 'keySet' => ['keys' => [[1,1]]], + 'transactionId' => $transactionName, + 'session' => $sessionName, + 'table' => $tableName, + 'columns' => $columns, + 'database' => $databaseName, + ], + [$sessionName, $tableName, $columns, $keySetComposite, ['transaction' => $transactionSelector, 'userHeaders' => ['google-cloud-resource-prefix' => [$databaseName]]]] + ], // test read write [ 'beginTransaction', From 06239ea37a6551c026a67413f3153679d742ca19 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 18 May 2017 23:35:32 -0400 Subject: [PATCH 26/46] Prepare v0.30.1 (#501) * Prepare v0.30.1 * Bump cloud-core version * Bump proto-client-php version --- docs/manifest.json | 5 +++++ src/Core/VERSION | 2 +- src/ErrorReporting/VERSION | 2 +- src/ErrorReporting/composer.json | 2 +- src/Monitoring/VERSION | 2 +- src/Monitoring/composer.json | 2 +- src/ServiceBuilder.php | 2 +- src/Spanner/SpannerClient.php | 2 +- src/Spanner/VERSION | 2 +- src/Spanner/composer.json | 2 +- 10 files changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index dcae5012050e..db0f8849bf35 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -11,6 +11,7 @@ "name": "google/cloud", "defaultService": "servicebuilder", "versions": [ + "v0.30.1", "v0.30.0", "v0.29.0", "v0.28.0", @@ -64,6 +65,7 @@ "name": "google/cloud-core", "defaultService": "core/readme", "versions": [ + "v1.4.1", "v1.4.0", "v1.3.0", "v1.2.0", @@ -90,6 +92,7 @@ "name": "google/cloud-error-reporting", "defaultService": "errorreporting/readme", "versions": [ + "v0.2.1", "v0.2.0", "v0.1.0", "master" @@ -112,6 +115,7 @@ "name": "google/cloud-monitoring", "defaultService": "monitoring/readme", "versions": [ + "v0.2.1", "v0.2.0", "v0.1.0", "master" @@ -146,6 +150,7 @@ "name": "google/cloud-spanner", "defaultService": "spanner/spannerclient", "versions": [ + "v0.1.1", "v0.1.0", "master" ] diff --git a/src/Core/VERSION b/src/Core/VERSION index e21e727f96fa..13175fdc4371 100644 --- a/src/Core/VERSION +++ b/src/Core/VERSION @@ -1 +1 @@ -1.4.0 \ No newline at end of file +1.4.1 \ No newline at end of file diff --git a/src/ErrorReporting/VERSION b/src/ErrorReporting/VERSION index 341cf11faf9a..7dff5b892112 100644 --- a/src/ErrorReporting/VERSION +++ b/src/ErrorReporting/VERSION @@ -1 +1 @@ -0.2.0 \ No newline at end of file +0.2.1 \ No newline at end of file diff --git a/src/ErrorReporting/composer.json b/src/ErrorReporting/composer.json index fbd0a494d34a..a66d81853091 100644 --- a/src/ErrorReporting/composer.json +++ b/src/ErrorReporting/composer.json @@ -6,7 +6,7 @@ "require": { "ext-grpc": "*", "google/cloud-core": "^1.0", - "google/proto-client-php": "^0.9", + "google/proto-client-php": "^0.13", "google/gax": "^0.8" }, "extra": { diff --git a/src/Monitoring/VERSION b/src/Monitoring/VERSION index 341cf11faf9a..7dff5b892112 100644 --- a/src/Monitoring/VERSION +++ b/src/Monitoring/VERSION @@ -1 +1 @@ -0.2.0 \ No newline at end of file +0.2.1 \ No newline at end of file diff --git a/src/Monitoring/composer.json b/src/Monitoring/composer.json index b67dc35fe88e..ef8c2abc87ff 100644 --- a/src/Monitoring/composer.json +++ b/src/Monitoring/composer.json @@ -6,7 +6,7 @@ "require": { "ext-grpc": "*", "google/cloud-core": "^1.0", - "google/proto-client-php": "^0.9", + "google/proto-client-php": "^0.13", "google/gax": "^0.8" }, "extra": { diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index d2198b842a5b..05a7f95c9243 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -50,7 +50,7 @@ */ class ServiceBuilder { - const VERSION = '0.30.0'; + const VERSION = '0.30.1'; /** * @var array Configuration options to be used between clients. diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 878a0900eb5d..2ed3b427e72a 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -65,7 +65,7 @@ class SpannerClient use LROTrait; use ValidateTrait; - const VERSION = '0.1.0'; + const VERSION = '0.1.1'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data'; const ADMIN_SCOPE = 'https://www.googleapis.com/auth/spanner.admin'; diff --git a/src/Spanner/VERSION b/src/Spanner/VERSION index 6c6aa7cb0918..6da28dde76d6 100644 --- a/src/Spanner/VERSION +++ b/src/Spanner/VERSION @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.1.1 \ No newline at end of file diff --git a/src/Spanner/composer.json b/src/Spanner/composer.json index 64d1b341b7e3..8c8749e150ce 100644 --- a/src/Spanner/composer.json +++ b/src/Spanner/composer.json @@ -7,7 +7,7 @@ "ext-grpc": "*", "google/cloud-core": "^1.4", "google/gax": "^0.9", - "google/proto-client-php": "^0.8" + "google/proto-client-php": "^0.13" }, "suggest": { "symfony/lock": "Required for the default session handler. Should be included as follows: symfony/lock:dev-master#1ba6ac9" From 216f51955858f4109ebe3bc654d5bdb4478991d8 Mon Sep 17 00:00:00 2001 From: michaelbausor Date: Fri, 19 May 2017 14:22:15 -0700 Subject: [PATCH 27/46] Update GAPICs for Spanner (#508) * Update spanner gapic * Update video intelligence * Update Spanner, VideoIntelligence and Speech * Address PR feedback --- .../Admin/Database/V1/DatabaseAdminClient.php | 45 +++++++++++-------- .../database_admin_client_config.json | 14 +++--- .../Admin/Instance/V1/InstanceAdminClient.php | 43 ++++++++++-------- .../instance_admin_client_config.json | 14 +++--- src/Spanner/V1/SpannerClient.php | 45 ++++++++++--------- .../V1/resources/spanner_client_config.json | 14 +++--- src/Speech/V1beta1/SpeechClient.php | 32 ++++++------- .../resources/speech_client_config.json | 4 +- .../VideoIntelligenceServiceClient.php | 14 +++--- 9 files changed, 121 insertions(+), 104 deletions(-) diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php index 332d10cc7728..baec24318a42 100644 --- a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php @@ -1,6 +1,6 @@ RetryOptions, where the keys * are method names (e.g. 'createFoo'), that overrides default retrying @@ -310,9 +321,6 @@ public function resumeOperation($operationName, $methodName = null) * that don't use retries. For calls that use retries, * set the timeout in RetryOptions. * Default: 30000 (30 seconds) - * @type string $appName The codename of the calling service. Default 'gax'. - * @type string $appVersion The version of the calling service. - * Default: the current version of GAX. * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. @@ -329,27 +337,26 @@ public function __construct($options = []) ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); if (array_key_exists('operationsClient', $options)) { $this->operationsClient = $options['operationsClient']; } else { - $this->operationsClient = new OperationsClient([ - 'serviceAddress' => $options['serviceAddress'], - 'scopes' => $options['scopes'], - ]); + $operationsClientOptions = $options; + unset($operationsClientOptions['timeoutMillis']); + unset($operationsClientOptions['retryingOverride']); + $this->operationsClient = new OperationsClient($operationsClientOptions); } + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; @@ -701,7 +708,7 @@ public function getDatabase($name, $optionalArgs = []) * [longrunning.Operation][google.longrunning.Operation]: `/operations/`. * * `operation_id` should be unique within the database, and must be - * a valid identifier: `[a-zA-Z][a-zA-Z0-9_]*`. Note that + * a valid identifier: `[a-z][a-z0-9_]*`. Note that * automatically-generated operation IDs always begin with an * underscore. If the named operation already exists, * [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] returns diff --git a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json index efa919a0a7d8..bba6763e0100 100644 --- a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json +++ b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.spanner.admin.database.v1.DatabaseAdmin": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php index bb7d0a10b257..8db5e5121850 100644 --- a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -1,6 +1,6 @@ RetryOptions, where the keys * are method names (e.g. 'createFoo'), that overrides default retrying @@ -356,9 +367,6 @@ public function resumeOperation($operationName, $methodName = null) * that don't use retries. For calls that use retries, * set the timeout in RetryOptions. * Default: 30000 (30 seconds) - * @type string $appName The codename of the calling service. Default 'gax'. - * @type string $appVersion The version of the calling service. - * Default: the current version of GAX. * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. @@ -375,27 +383,26 @@ public function __construct($options = []) ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); if (array_key_exists('operationsClient', $options)) { $this->operationsClient = $options['operationsClient']; } else { - $this->operationsClient = new OperationsClient([ - 'serviceAddress' => $options['serviceAddress'], - 'scopes' => $options['scopes'], - ]); + $operationsClientOptions = $options; + unset($operationsClientOptions['timeoutMillis']); + unset($operationsClientOptions['retryingOverride']); + $this->operationsClient = new OperationsClient($operationsClientOptions); } + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json index 23dbca4fe655..c438b46bd79a 100644 --- a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json +++ b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.spanner.admin.instance.v1.InstanceAdmin": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index 21f8dc2bdfbe..55417e52e6f6 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -1,6 +1,6 @@ RetryOptions, where the keys * are method names (e.g. 'createFoo'), that overrides default retrying @@ -261,9 +271,6 @@ private static function getGrpcStreamingDescriptors() * that don't use retries. For calls that use retries, * set the timeout in RetryOptions. * Default: 30000 (30 seconds) - * @type string $appName The codename of the calling service. Default 'gax'. - * @type string $appVersion The version of the calling service. - * Default: the current version of GAX. * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. @@ -280,18 +287,17 @@ public function __construct($options = []) ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; @@ -360,10 +366,9 @@ public function __construct($options = []) * * Cloud Spanner limits the number of sessions that can exist at any given * time; thus, it is a good idea to delete idle and/or unneeded sessions. - * Aside from explicit deletes, Cloud Spanner can delete sessions for - * which no operations are sent for more than an hour, or due to - * internal errors. If a session is deleted, requests to it - * return `NOT_FOUND`. + * Aside from explicit deletes, Cloud Spanner can delete sessions for which no + * operations are sent for more than an hour. If a session is deleted, + * requests to it return `NOT_FOUND`. * * Idle sessions can be kept alive by sending a trivial SQL query * periodically, e.g., `"SELECT 1"`. @@ -959,7 +964,7 @@ public function streamingRead($session, $table, $columns, $keySet, $optionalArgs $mergedSettings, $this->descriptors['streamingRead'] ); -// print_r($request->serialize(new PhpArray));exit; + return $callable( $request, [], diff --git a/src/Spanner/V1/resources/spanner_client_config.json b/src/Spanner/V1/resources/spanner_client_config.json index db4ced68c440..76c56b6880c7 100644 --- a/src/Spanner/V1/resources/spanner_client_config.json +++ b/src/Spanner/V1/resources/spanner_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.spanner.v1.Spanner": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/Speech/V1beta1/SpeechClient.php b/src/Speech/V1beta1/SpeechClient.php index 2e2e7997c0d1..d4a0bca2086f 100644 --- a/src/Speech/V1beta1/SpeechClient.php +++ b/src/Speech/V1beta1/SpeechClient.php @@ -223,12 +223,10 @@ public function __construct($options = []) if (array_key_exists('operationsClient', $options)) { $this->operationsClient = $options['operationsClient']; } else { - $this->operationsClient = new OperationsClient([ - 'serviceAddress' => $options['serviceAddress'], - 'scopes' => $options['scopes'], - 'libName' => $options['libName'], - 'libVersion' => $options['libVersion'], - ]); + $operationsClientOptions = $options; + unset($operationsClientOptions['timeoutMillis']); + unset($operationsClientOptions['retryingOverride']); + $this->operationsClient = new OperationsClient($operationsClientOptions); } $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); @@ -289,7 +287,7 @@ public function __construct($options = []) } /** - * Perform synchronous speech-recognition: receive results after all audio + * Performs synchronous speech recognition: receive results after all audio * has been sent and processed. * * Sample code: @@ -310,9 +308,9 @@ public function __construct($options = []) * } * ``` * - * @param RecognitionConfig $config [Required] The `config` message provides information to the recognizer - * that specifies how to process the request. - * @param RecognitionAudio $audio [Required] The audio data to be recognized. + * @param RecognitionConfig $config *Required* Provides information to the recognizer that specifies how to + * process the request. + * @param RecognitionAudio $audio *Required* The audio data to be recognized. * @param array $optionalArgs { * Optional. * @@ -351,8 +349,10 @@ public function syncRecognize($config, $audio, $optionalArgs = []) } /** - * Perform asynchronous speech-recognition: receive results via the - * google.longrunning.Operations interface. Returns either an + * Performs asynchronous speech recognition: receive results via the + * [google.longrunning.Operations] + * (https://cloud.google.com/speech/reference/rest/v1beta1/operations#Operation) + * interface. Returns either an * `Operation.error` or an `Operation.response` which contains * an `AsyncRecognizeResponse` message. * @@ -399,9 +399,9 @@ public function syncRecognize($config, $audio, $optionalArgs = []) * } * ``` * - * @param RecognitionConfig $config [Required] The `config` message provides information to the recognizer - * that specifies how to process the request. - * @param RecognitionAudio $audio [Required] The audio data to be recognized. + * @param RecognitionConfig $config *Required* Provides information to the recognizer that specifies how to + * process the request. + * @param RecognitionAudio $audio *Required* The audio data to be recognized. * @param array $optionalArgs { * Optional. * @@ -440,7 +440,7 @@ public function asyncRecognize($config, $audio, $optionalArgs = []) } /** - * Perform bidirectional streaming speech-recognition: receive results while + * Performs bidirectional streaming speech recognition: receive results while * sending audio. This method is only available via the gRPC API (not REST). * * Sample code: diff --git a/src/Speech/V1beta1/resources/speech_client_config.json b/src/Speech/V1beta1/resources/speech_client_config.json index 5d11ce19e587..e1877f31ec57 100644 --- a/src/Speech/V1beta1/resources/speech_client_config.json +++ b/src/Speech/V1beta1/resources/speech_client_config.json @@ -15,9 +15,9 @@ "initial_retry_delay_millis": 100, "retry_delay_multiplier": 1.3, "max_retry_delay_millis": 60000, - "initial_rpc_timeout_millis": 60000, + "initial_rpc_timeout_millis": 190000, "rpc_timeout_multiplier": 1.0, - "max_rpc_timeout_millis": 60000, + "max_rpc_timeout_millis": 190000, "total_timeout_millis": 600000 } }, diff --git a/src/VideoIntelligence/V1beta1/VideoIntelligenceServiceClient.php b/src/VideoIntelligence/V1beta1/VideoIntelligenceServiceClient.php index a283ef134b70..5fcc8f8a66a5 100644 --- a/src/VideoIntelligence/V1beta1/VideoIntelligenceServiceClient.php +++ b/src/VideoIntelligence/V1beta1/VideoIntelligenceServiceClient.php @@ -232,12 +232,10 @@ public function __construct($options = []) if (array_key_exists('operationsClient', $options)) { $this->operationsClient = $options['operationsClient']; } else { - $this->operationsClient = new OperationsClient([ - 'serviceAddress' => $options['serviceAddress'], - 'scopes' => $options['scopes'], - 'libName' => $options['libName'], - 'libVersion' => $options['libVersion'], - ]); + $operationsClientOptions = $options; + unset($operationsClientOptions['timeoutMillis']); + unset($operationsClientOptions['retryingOverride']); + $this->operationsClient = new OperationsClient($operationsClientOptions); } $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); @@ -339,7 +337,7 @@ public function __construct($options = []) * supported, which must be specified in the following format: * `gs://bucket-id/object-id` (other URI formats return * [google.rpc.Code.INVALID_ARGUMENT][google.rpc.Code.INVALID_ARGUMENT]). For more information, see - * [Request URIs](/storage/docs/reference-uris). + * [Request URIs](https://cloud.google.com/storage/docs/reference-uris). * A video URI may include wildcards in `object-id`, and thus identify * multiple videos. Supported wildcards: '*' to match 0 or more characters; * '?' to match 1 character. If unset, the input video should be embedded @@ -360,7 +358,7 @@ public function __construct($options = []) * URIs are supported, which must be specified in the following format: * `gs://bucket-id/object-id` (other URI formats return * [google.rpc.Code.INVALID_ARGUMENT][google.rpc.Code.INVALID_ARGUMENT]). For more information, see - * [Request URIs](/storage/docs/reference-uris). + * [Request URIs](https://cloud.google.com/storage/docs/reference-uris). * @type string $locationId * Optional cloud region where annotation should take place. Supported cloud * regions: `us-east1`, `us-west1`, `europe-west1`, `asia-east1`. If no region From b0ecc19aa9d3cb50399107ad2436cc3125757ee7 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Mon, 22 May 2017 12:05:56 -0400 Subject: [PATCH 28/46] Modify exceptions when a whitelist may be present (#482) * Modify exceptions when a whitelist may be present * Add Whitelist system tests * Code review fixes * Refactor out additional call_user_func --- src/Core/Exception/NotFoundException.php | 12 +- src/Core/GrpcRequestWrapper.php | 1 + src/Core/GrpcTrait.php | 17 +- src/Core/RestTrait.php | 28 ++- src/Core/WhitelistTrait.php | 39 ++++ src/PubSub/Connection/Grpc.php | 12 +- src/PubSub/Connection/Rest.php | 12 +- .../system/ServiceWhitelist/WhitelistTest.php | 182 ++++++++++++++++++ tests/system/bootstrap.php | 6 + .../Core/Exception/NotFoundExceptionTest.php | 36 ++++ tests/unit/Core/WhitelistTraitTest.php | 58 ++++++ 11 files changed, 383 insertions(+), 20 deletions(-) create mode 100644 src/Core/WhitelistTrait.php create mode 100644 tests/system/ServiceWhitelist/WhitelistTest.php create mode 100644 tests/unit/Core/Exception/NotFoundExceptionTest.php create mode 100644 tests/unit/Core/WhitelistTraitTest.php diff --git a/src/Core/Exception/NotFoundException.php b/src/Core/Exception/NotFoundException.php index f0a1f3139ed6..e6ba2c5cbe59 100644 --- a/src/Core/Exception/NotFoundException.php +++ b/src/Core/Exception/NotFoundException.php @@ -22,5 +22,15 @@ */ class NotFoundException extends ServiceException { - + /** + * Allows overriding message for injection of Whitelist Notice. + * + * @param string $message the new message + * @return void + * @access private + */ + public function setMessage($message) + { + $this->message = $message; + } } diff --git a/src/Core/GrpcRequestWrapper.php b/src/Core/GrpcRequestWrapper.php index 3d89d417a9e0..51cb3a3f1a07 100644 --- a/src/Core/GrpcRequestWrapper.php +++ b/src/Core/GrpcRequestWrapper.php @@ -216,6 +216,7 @@ private function convertToGoogleException(ApiException $ex) break; case Grpc\STATUS_NOT_FOUND: + case Grpc\STATUS_UNIMPLEMENTED: $exception = Exception\NotFoundException::class; break; diff --git a/src/Core/GrpcTrait.php b/src/Core/GrpcTrait.php index 46313151a373..01a655e3e5d9 100644 --- a/src/Core/GrpcTrait.php +++ b/src/Core/GrpcTrait.php @@ -19,9 +19,10 @@ use DateTime; use DateTimeZone; -use Google\Auth\FetchAuthTokenCache; use Google\Auth\Cache\MemoryCacheItemPool; +use Google\Auth\FetchAuthTokenCache; use Google\Cloud\Core\ArrayTrait; +use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\GrpcRequestWrapper; use google\protobuf; @@ -31,6 +32,7 @@ trait GrpcTrait { use ArrayTrait; + use WhitelistTrait; /** * @var GrpcRequestWrapper Wrapper used to handle sending requests to the @@ -53,9 +55,10 @@ public function setRequestWrapper(GrpcRequestWrapper $requestWrapper) * * @param callable $request * @param array $args + * @param bool $whitelisted * @return \Generator|array */ - public function send(callable $request, array $args) + public function send(callable $request, array $args, $whitelisted = false) { $requestOptions = $this->pluckArray([ 'grpcOptions', @@ -63,7 +66,15 @@ public function send(callable $request, array $args) 'requestTimeout' ], $args[count($args) - 1]); - return $this->requestWrapper->send($request, $args, $requestOptions); + try { + return $this->requestWrapper->send($request, $args, $requestOptions); + } catch (NotFoundException $e) { + if ($whitelisted) { + throw $this->modifyWhitelistedError($e); + } + + throw $e; + } } /** diff --git a/src/Core/RestTrait.php b/src/Core/RestTrait.php index 663687a807e9..1640e5baad55 100644 --- a/src/Core/RestTrait.php +++ b/src/Core/RestTrait.php @@ -17,6 +17,8 @@ namespace Google\Cloud\Core; +use Google\Cloud\Core\Exception\NotFoundException; + /** * Provides shared functionality for REST service implementations. */ @@ -24,6 +26,7 @@ trait RestTrait { use ArrayTrait; use JsonTrait; + use WhitelistTrait; /** * @var RequestBuilder Builds PSR7 requests from a service definition. @@ -64,9 +67,10 @@ public function setRequestWrapper(RequestWrapper $requestWrapper) * @param string $resource The resource type used for the request. * @param string $method The method used for the request. * @param array $options [optional] Options used to build out the request. + * @param array $whitelisted [optional] * @return array */ - public function send($resource, $method, array $options = []) + public function send($resource, $method, array $options = [], $whitelisted = false) { $requestOptions = $this->pluckArray([ 'restOptions', @@ -74,12 +78,20 @@ public function send($resource, $method, array $options = []) 'requestTimeout' ], $options); - return json_decode( - $this->requestWrapper->send( - $this->requestBuilder->build($resource, $method, $options), - $requestOptions - )->getBody(), - true - ); + try { + return json_decode( + $this->requestWrapper->send( + $this->requestBuilder->build($resource, $method, $options), + $requestOptions + )->getBody(), + true + ); + } catch (NotFoundException $e) { + if ($whitelisted) { + throw $this->modifyWhitelistedError($e); + } + + throw $e; + } } } diff --git a/src/Core/WhitelistTrait.php b/src/Core/WhitelistTrait.php new file mode 100644 index 000000000000..5e40e6bef7a3 --- /dev/null +++ b/src/Core/WhitelistTrait.php @@ -0,0 +1,39 @@ +setMessage('NOTE: Error may be due to Whitelist Restriction. ' . $e->getMessage()); + + return $e; + } +} diff --git a/src/PubSub/Connection/Grpc.php b/src/PubSub/Connection/Grpc.php index 445c7a6be946..903de3fc860e 100644 --- a/src/PubSub/Connection/Grpc.php +++ b/src/PubSub/Connection/Grpc.php @@ -288,10 +288,11 @@ public function acknowledge(array $args) */ public function listSnapshots(array $args) { + $whitelisted = true; return $this->send([$this->subscriberClient, 'listSnapshots'], [ $this->pluck('project', $args), $args - ]); + ], $whitelisted); } /** @@ -299,11 +300,12 @@ public function listSnapshots(array $args) */ public function createSnapshot(array $args) { + $whitelisted = true; return $this->send([$this->subscriberClient, 'createSnapshot'], [ $this->pluck('name', $args), $this->pluck('subscription', $args), $args - ]); + ], $whitelisted); } /** @@ -311,10 +313,11 @@ public function createSnapshot(array $args) */ public function deleteSnapshot(array $args) { + $whitelisted = true; return $this->send([$this->subscriberClient, 'deleteSnapshot'], [ $this->pluck('snapshot', $args), $args - ]); + ], $whitelisted); } /** @@ -327,10 +330,11 @@ public function seek(array $args) $args['time'] = (new protobuf\Timestamp)->deserialize($time, $this->codec); } + $whitelisted = true; return $this->send([$this->subscriberClient, 'seek'], [ $this->pluck('subscription', $args), $args - ]); + ], $whitelisted); } /** diff --git a/src/PubSub/Connection/Rest.php b/src/PubSub/Connection/Rest.php index 44bbb65e7fcc..d45e9b4b2d15 100644 --- a/src/PubSub/Connection/Rest.php +++ b/src/PubSub/Connection/Rest.php @@ -214,7 +214,8 @@ public function acknowledge(array $args) */ public function listSnapshots(array $args) { - return $this->send('snapshots', 'list', $args); + $whitelisted = true; + return $this->send('snapshots', 'list', $args, $whitelisted); } /** @@ -222,7 +223,8 @@ public function listSnapshots(array $args) */ public function createSnapshot(array $args) { - return $this->send('snapshots', 'create', $args); + $whitelisted = true; + return $this->send('snapshots', 'create', $args, $whitelisted); } /** @@ -230,7 +232,8 @@ public function createSnapshot(array $args) */ public function deleteSnapshot(array $args) { - return $this->send('snapshots', 'delete', $args); + $whitelisted = true; + return $this->send('snapshots', 'delete', $args, $whitelisted); } /** @@ -238,7 +241,8 @@ public function deleteSnapshot(array $args) */ public function seek(array $args) { - return $this->send('subscriptions', 'seek', $args); + $whitelisted = true; + return $this->send('subscriptions', 'seek', $args, $whitelisted); } /** diff --git a/tests/system/ServiceWhitelist/WhitelistTest.php b/tests/system/ServiceWhitelist/WhitelistTest.php new file mode 100644 index 000000000000..cbb86b347cc7 --- /dev/null +++ b/tests/system/ServiceWhitelist/WhitelistTest.php @@ -0,0 +1,182 @@ +markTestSkipped('Missing whitelist keyfile path for whitelist system tests.'); + } + + $this->keyFilePath = GOOGLE_CLOUD_WHITELIST_KEY_PATH; + } + + public function testPubSubListSnapshotsRest() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'rest' + ]); + + $this->checkException(function() use ($client) { + iterator_to_array($client->snapshots()); + }); + } + + public function testPubSubListSnapshotsGrpc() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'grpc' + ]); + + $this->checkException(function() use ($client) { + iterator_to_array($client->snapshots()); + }); + } + + public function testPubSubCreateSnapshotRest() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'rest' + ]); + + $topic = $client->createTopic(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($topic) { + $topic->delete(); + }; + + $sub = $topic->subscribe(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($sub) { + $sub->delete(); + }; + + $this->checkException(function () use ($client, $sub) { + $client->createSnapshot(uniqid(self::TESTING_PREFIX), $sub); + }); + } + + public function testPubSubCreateSnapshotGrpc() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'grpc' + ]); + + $topic = $client->createTopic(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($topic) { + $topic->delete(); + }; + + $sub = $topic->subscribe(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($sub) { + $sub->delete(); + }; + + $this->checkException(function () use ($client, $sub) { + $client->createSnapshot(uniqid(self::TESTING_PREFIX), $sub); + }); + } + + public function testPubSubSeekRest() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'rest' + ]); + + $topic = $client->createTopic(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($topic) { + $topic->delete(); + }; + + $sub = $topic->subscribe(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($sub) { + $sub->delete(); + }; + + $this->checkException(function () use ($sub) { + $sub->seekToTime(new Timestamp(new \DateTime)); + }); + } + + public function testPubSubSeekGrpc() + { + $client = new PubSubClient([ + 'keyFilePath' => $this->keyFilePath, + 'transport' => 'grpc' + ]); + + $topic = $client->createTopic(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($topic) { + $topic->delete(); + }; + + $sub = $topic->subscribe(uniqid(self::TESTING_PREFIX)); + self::$deletionQueue[] = function () use ($sub) { + $sub->delete(); + }; + + $this->checkException(function () use ($sub) { + $sub->seekToTime(new Timestamp(new \DateTime)); + }); + } + + private function checkException(callable $call) + { + $thrown = false; + $ex = null; + try { + $call(); + } catch (\Exception $e) { + $thrown = true; + $ex = $e; + } + + $this->assertTrue($thrown); + $this->assertInstanceOf(NotFoundException::class, $ex); + $this->assertTrue(strpos($ex->getMessage(), self::MESSAGE) !== false); + } + + public static function tearDownFixtures() + { + foreach (self::$deletionQueue as $toDelete) { + if (!is_callable($toDelete)) { + throw new \Exception('fixtures must be callables'); + } + + call_user_func($toDelete); + } + } +} diff --git a/tests/system/bootstrap.php b/tests/system/bootstrap.php index f9e2b36a997c..528369062539 100644 --- a/tests/system/bootstrap.php +++ b/tests/system/bootstrap.php @@ -8,6 +8,7 @@ use Google\Cloud\Tests\System\PubSub\PubSubTestCase; use Google\Cloud\Tests\System\Spanner\SpannerTestCase; use Google\Cloud\Tests\System\Storage\StorageTestCase; +use Google\Cloud\Tests\System\Whitelist\WhitelistTest; if (!getenv('GOOGLE_CLOUD_PHP_TESTS_KEY_PATH')) { throw new \Exception( @@ -15,6 +16,10 @@ ); } +if (getenv('GOOGLE_CLOUD_PHP_TESTS_WHITELIST_KEY_PATH')) { + define('GOOGLE_CLOUD_WHITELIST_KEY_PATH', getenv('GOOGLE_CLOUD_PHP_TESTS_WHITELIST_KEY_PATH')); +} + register_shutdown_function(function () { PubSubTestCase::tearDownFixtures(); DatastoreTestCase::tearDownFixtures(); @@ -22,4 +27,5 @@ LoggingTestCase::tearDownFixtures(); BigQueryTestCase::tearDownFixtures(); SpannerTestCase::tearDownFixtures(); + WhitelistTest::tearDownFixtures(); }); diff --git a/tests/unit/Core/Exception/NotFoundExceptionTest.php b/tests/unit/Core/Exception/NotFoundExceptionTest.php new file mode 100644 index 000000000000..72dd944a5f67 --- /dev/null +++ b/tests/unit/Core/Exception/NotFoundExceptionTest.php @@ -0,0 +1,36 @@ +assertEquals($ex->getMessage(), 'hello'); + + $ex->setMessage('world'); + $this->assertEquals($ex->getMessage(), 'world'); + } +} diff --git a/tests/unit/Core/WhitelistTraitTest.php b/tests/unit/Core/WhitelistTraitTest.php new file mode 100644 index 000000000000..dd654c205919 --- /dev/null +++ b/tests/unit/Core/WhitelistTraitTest.php @@ -0,0 +1,58 @@ +trait = new WhitelistTraitStub; + } + + public function testModifyWhitelistedError() + { + $ex = new NotFoundException('hello world'); + + $res = $this->trait->call('modifyWhitelistedError', [$ex]); + + $this->assertInstanceOf(NotFoundException::class, $res); + $this->assertEquals( + $res->getMessage(), + 'NOTE: Error may be due to Whitelist Restriction. hello world' + ); + } +} + +class WhitelistTraitStub +{ + use WhitelistTrait; + + public function call($method, array $args) + { + return call_user_func_array([$this, $method], $args); + } +} From 53afa7ad5dbb245313f327ad1dce861d57df8fea Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Mon, 22 May 2017 12:18:33 -0400 Subject: [PATCH 29/46] Prepare v0.31.0 (#510) --- docs/manifest.json | 5 +++++ src/Core/VERSION | 2 +- src/PubSub/PubSubClient.php | 2 +- src/PubSub/VERSION | 2 +- src/ServiceBuilder.php | 2 +- src/Spanner/SpannerClient.php | 2 +- src/Spanner/VERSION | 2 +- src/Speech/SpeechClient.php | 2 +- src/Speech/VERSION | 2 +- 9 files changed, 13 insertions(+), 8 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index db0f8849bf35..587ec983cf2b 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -11,6 +11,7 @@ "name": "google/cloud", "defaultService": "servicebuilder", "versions": [ + "v0.31.0", "v0.30.1", "v0.30.0", "v0.29.0", @@ -65,6 +66,7 @@ "name": "google/cloud-core", "defaultService": "core/readme", "versions": [ + "v1.5.0", "v1.4.1", "v1.4.0", "v1.3.0", @@ -137,6 +139,7 @@ "name": "google/cloud-pubsub", "defaultService": "pubsub/pubsubclient", "versions": [ + "v0.5.1", "v0.5.0", "v0.4.0", "v0.3.0", @@ -150,6 +153,7 @@ "name": "google/cloud-spanner", "defaultService": "spanner/spannerclient", "versions": [ + "v0.2.0", "v0.1.1", "v0.1.0", "master" @@ -160,6 +164,7 @@ "name": "google/cloud-speech", "defaultService": "speech/speechclient", "versions": [ + "v0.4.0", "v0.3.0", "v0.2.0", "v0.1.0", diff --git a/src/Core/VERSION b/src/Core/VERSION index 13175fdc4371..3e1ad720b13d 100644 --- a/src/Core/VERSION +++ b/src/Core/VERSION @@ -1 +1 @@ -1.4.1 \ No newline at end of file +1.5.0 \ No newline at end of file diff --git a/src/PubSub/PubSubClient.php b/src/PubSub/PubSubClient.php index 588cab9a0df8..74d9e50da065 100644 --- a/src/PubSub/PubSubClient.php +++ b/src/PubSub/PubSubClient.php @@ -85,7 +85,7 @@ class PubSubClient use IncomingMessageTrait; use ResourceNameTrait; - const VERSION = '0.5.0'; + const VERSION = '0.5.1'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/pubsub'; diff --git a/src/PubSub/VERSION b/src/PubSub/VERSION index 79a2734bbf3d..5d4294b9120c 100644 --- a/src/PubSub/VERSION +++ b/src/PubSub/VERSION @@ -1 +1 @@ -0.5.0 \ No newline at end of file +0.5.1 \ No newline at end of file diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index 05a7f95c9243..13e838b62b57 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -50,7 +50,7 @@ */ class ServiceBuilder { - const VERSION = '0.30.1'; + const VERSION = '0.31.0'; /** * @var array Configuration options to be used between clients. diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 2ed3b427e72a..a19803a68d5a 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -65,7 +65,7 @@ class SpannerClient use LROTrait; use ValidateTrait; - const VERSION = '0.1.1'; + const VERSION = '0.2.0'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data'; const ADMIN_SCOPE = 'https://www.googleapis.com/auth/spanner.admin'; diff --git a/src/Spanner/VERSION b/src/Spanner/VERSION index 6da28dde76d6..341cf11faf9a 100644 --- a/src/Spanner/VERSION +++ b/src/Spanner/VERSION @@ -1 +1 @@ -0.1.1 \ No newline at end of file +0.2.0 \ No newline at end of file diff --git a/src/Speech/SpeechClient.php b/src/Speech/SpeechClient.php index 423f8bf1a80b..74eaef419766 100644 --- a/src/Speech/SpeechClient.php +++ b/src/Speech/SpeechClient.php @@ -42,7 +42,7 @@ class SpeechClient { use ClientTrait; - const VERSION = '0.3.0'; + const VERSION = '0.4.0'; const SCOPE = 'https://www.googleapis.com/auth/cloud-platform'; diff --git a/src/Speech/VERSION b/src/Speech/VERSION index 9325c3ccda98..60a2d3e96c80 100644 --- a/src/Speech/VERSION +++ b/src/Speech/VERSION @@ -1 +1 @@ -0.3.0 \ No newline at end of file +0.4.0 \ No newline at end of file From 63d529bfc9691e489992be34b0fa297fc31341d2 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Mon, 22 May 2017 15:03:05 -0400 Subject: [PATCH 30/46] Prepare v0.31.1 (#511) --- docs/external-classes.json | 10 +++++----- docs/manifest.json | 2 ++ src/ServiceBuilder.php | 2 +- src/VideoIntelligence/VERSION | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/external-classes.json b/docs/external-classes.json index 97d764de7904..c7db19ec43cd 100644 --- a/docs/external-classes.json +++ b/docs/external-classes.json @@ -86,13 +86,13 @@ "name": "PushConfig", "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/pubsub/v1/pubsub.php#L1827" }, { - "name": "google\\cloud\\videointelligence\\v1beta1\\", - "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/videointelligence/v1beta1" + "name": "google\\cloud\\videointelligence\\v1beta1\\Feature", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/videointelligence/v1beta1/video_intelligence.php#8" }, { - "name": "VideoContext", + "name": "google\\cloud\\videointelligence\\v1beta1\\VideoContext", "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/videointelligence/v1beta1/video_intelligence.php#L379" }, { - "name": "google\\cloud\\videointelligence\\v1beta1\\Feature", - "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/videointelligence/v1beta1/video_intelligence.php#8" + "name": "google\\cloud\\videointelligence\\v1beta1\\", + "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/videointelligence/v1beta1/video_intelligence.php" }] diff --git a/docs/manifest.json b/docs/manifest.json index 587ec983cf2b..92c98685ded1 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -11,6 +11,7 @@ "name": "google/cloud", "defaultService": "servicebuilder", "versions": [ + "v0.31.1", "v0.31.0", "v0.30.1", "v0.30.0", @@ -196,6 +197,7 @@ "name": "google/cloud-videointelligence", "defaultService": "videointelligence/videointelligenceserviceclient", "versions": [ + "v0.2.0", "v0.1.0", "master" ] diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index 13e838b62b57..f38af13b9299 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -50,7 +50,7 @@ */ class ServiceBuilder { - const VERSION = '0.31.0'; + const VERSION = '0.31.1'; /** * @var array Configuration options to be used between clients. diff --git a/src/VideoIntelligence/VERSION b/src/VideoIntelligence/VERSION index 6c6aa7cb0918..341cf11faf9a 100644 --- a/src/VideoIntelligence/VERSION +++ b/src/VideoIntelligence/VERSION @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.2.0 \ No newline at end of file From c8648955aebc74f5093cf124cbe4e83947f149d3 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Tue, 23 May 2017 16:12:43 -0700 Subject: [PATCH 31/46] Request Tracing (#456) * Request tracing (#4) * Implement the Trace REST API Client. Implements basic Trace and TraceSpan resources. TraceClient tests Fix tests. Connection needed to be protected Code style fixes * Rename some methods, move a required parameter * remove labels class for now * Refactor TraceSpan to store attributes directly in the info array * Fix Trace namespace in ServiceBuilder * Rename some getter methods. Whitelist attributes in TraceSpan constructor * Fix aliases for Core classes * Request tracing prototype * Create more namespaces * RequestTracer should return results * Add labels to the primary span detected from the response. * Snippet tests and phpcs fixes * Fix cs * add Trace test * Add system tests for using the TraceClient * list traces test depends on creating the traces * Copyrights and documentation comments * remove debug statement * Fix some typehinting and documentation * Refactor Trace to lazy load span data if not set. * Convert listTraces output from a generator to an ItemIterator * Add @see note for Trace list function * Fix Trace system tests * Add TraceSpan factory function on Trace * Add test for lazy fetching span data from the api * Fix RequestTracer to work with updated apis * Make RequestTracer an instance. Static functions are redirected to the singleton instance of RequestTracer * Add options to trace insert and trace get calls * Remove REST Connection emulator hooks - there is no Trace emulator. * Trace#info no longer checks to see if you have already specified spans * Documentation updates * Remove projectId getter/setter from Trace. * cs ignore for long documentation links * Docs, fixing traces resultLimit, set timezone * Generate a span name from a cleaned backtrace * If the trace span is not created within a class, is the file's basename * Some backtrace entries do not have line numbers * Revert the change to info() that tried to maintain an info array * Add SamplerFactory * Add 3 more example trace reporters * Add GAE labels to the root span * fix missing semicolon * Fix GAE env variable parsing * Inject the cacheItemClass name into the QpsSampler. CacheItemPoolInterface implementations often are linked to their CacheItemInterface implementations in order to communicate expiry (not part of the PSR-6 spec. * Add a NullReporter. * TracerInterface now returns only spans rather than creating a trace record. Add tests * Add TraceContext class. * TraceSpan start and end times can accept numerics (timestamps) or strings * Documentation updates * close all spans at the end of the request * Adding version, license, docs for creating new google/cloud-trace package * Use factories for generating sampler and reporter from the initial RequestTracer::start() * Rename TraceReporter -> SyncReporter * Detect PSR-6 apc/apcu cache. Detect default start time from headers. * can't use pluck staticly * Docs and examples for RequestTracer * Remove ReporterFactory. Add documentation * Adding tests * SamplerFactory tests * Add suggested packages for the cache adapters * Add ExtensionTracer which will not work until the stackdriver extension is available * JsonSerializable TraceSpan * Fix serializable * Return trace context headers if provided to the request * Prefix service name in front of version if present * Extension tracer can handle initial trace context * Put the version header back to just the version * Add Trace section to main README * fix default spanId * finish -> end * Rename finish -> end for consistency across languages * Rename instrument -> inSpan to match other languages * Fix the label name for the module version for GAE * Update README for google/cloud-trace * TracerInterface no longer returns the span started or stopped. The extension tracer cannot return a reference to its internal representation of a TraceSpan. It would cause strange behavior to be able to modify the span object returned by reference. * ensure -> finally * Remove many unused label constants. Add load balancer labels. Add trace client version to agent * Test fixes * Separate the RequestTracer static methods from the RequestHandler instance. * Fix CS * Move default logger level to constructor * Cannot typehint floats * Add snippet tests * Implement the AsyncReporter that uses the BatchRunner * Add another search path for the autoloader for the batch daemon * Only need to use 2 worker threads by default * Removing AsyncReporter for now, until Batch package is back * clarify adding root label from adding label to current span * Add the addRootLabel to extension tracer * Adding tests for tracers and reporters * Fix EchoReporter test * Doc fixes * Documentation update for suggesting cache implementations * Documentation updates. is_a -> instanceof * Fix the snippet test syntax for updated dev tools * Spacing and doc updates for PR comments. * Documentation types, pin google/cloud-core version. SamplerFactory only handles arrays. * Documentation fixes * Handle failure cases for LoggerReporter and FileReporter * Add ramsey/uuid dependency for generating trace ids (uuids) * Remove extension tracer for now * Fix use statement for SamplerInterface --- composer.json | 3 +- src/Trace/IdGeneratorTrait.php | 36 +++ src/Trace/README.md | 96 +++++++ src/Trace/Reporter/EchoReporter.php | 39 +++ src/Trace/Reporter/FileReporter.php | 53 ++++ src/Trace/Reporter/LoggerReporter.php | 68 +++++ src/Trace/Reporter/NullReporter.php | 37 +++ src/Trace/Reporter/ReporterInterface.php | 34 +++ src/Trace/Reporter/SyncReporter.php | 66 +++++ src/Trace/RequestHandler.php | 259 ++++++++++++++++++ src/Trace/RequestTracer.php | 216 +++++++++++++++ src/Trace/Sampler/AlwaysOffSampler.php | 35 +++ src/Trace/Sampler/AlwaysOnSampler.php | 35 +++ src/Trace/Sampler/QpsSampler.php | 145 ++++++++++ src/Trace/Sampler/RandomSampler.php | 64 +++++ src/Trace/Sampler/SamplerFactory.php | 71 +++++ src/Trace/Sampler/SamplerInterface.php | 31 +++ src/Trace/Trace.php | 29 +- src/Trace/TraceContext.php | 166 +++++++++++ src/Trace/TraceSpan.php | 76 +++-- src/Trace/Tracer/ContextTracer.php | 169 ++++++++++++ src/Trace/Tracer/NullTracer.php | 109 ++++++++ src/Trace/Tracer/TracerInterface.php | 89 ++++++ .../{composer.json.temp => composer.json} | 8 +- tests/snippets/Trace/RequestTracerTest.php | 148 ++++++++++ tests/snippets/Trace/TraceClientTest.php | 2 - tests/snippets/Trace/TraceContextTest.php | 36 +++ .../unit/Trace/Reporter/EchoReporterTest.php | 56 ++++ .../unit/Trace/Reporter/FileReporterTest.php | 60 ++++ .../Trace/Reporter/LoggerReporterTest.php | 58 ++++ .../unit/Trace/Reporter/SyncReporterTest.php | 98 +++++++ tests/unit/Trace/RequestHandlerTest.php | 155 +++++++++++ tests/unit/Trace/RequestTracerTest.php | 56 ++++ tests/unit/Trace/Sampler/QpsSamplerTest.php | 84 ++++++ .../unit/Trace/Sampler/RandomSamplerTest.php | 44 +++ .../unit/Trace/Sampler/SamplerFactoryTest.php | 94 +++++++ tests/unit/Trace/TraceContextTest.php | 57 ++++ tests/unit/Trace/TraceSpanTest.php | 21 ++ tests/unit/Trace/TraceTest.php | 4 +- tests/unit/Trace/Tracer/ContextTracerTest.php | 83 ++++++ 40 files changed, 2938 insertions(+), 52 deletions(-) create mode 100644 src/Trace/IdGeneratorTrait.php create mode 100644 src/Trace/Reporter/EchoReporter.php create mode 100644 src/Trace/Reporter/FileReporter.php create mode 100644 src/Trace/Reporter/LoggerReporter.php create mode 100644 src/Trace/Reporter/NullReporter.php create mode 100644 src/Trace/Reporter/ReporterInterface.php create mode 100644 src/Trace/Reporter/SyncReporter.php create mode 100644 src/Trace/RequestHandler.php create mode 100644 src/Trace/RequestTracer.php create mode 100644 src/Trace/Sampler/AlwaysOffSampler.php create mode 100644 src/Trace/Sampler/AlwaysOnSampler.php create mode 100644 src/Trace/Sampler/QpsSampler.php create mode 100644 src/Trace/Sampler/RandomSampler.php create mode 100644 src/Trace/Sampler/SamplerFactory.php create mode 100644 src/Trace/Sampler/SamplerInterface.php create mode 100644 src/Trace/TraceContext.php create mode 100644 src/Trace/Tracer/ContextTracer.php create mode 100644 src/Trace/Tracer/NullTracer.php create mode 100644 src/Trace/Tracer/TracerInterface.php rename src/Trace/{composer.json.temp => composer.json} (62%) create mode 100644 tests/snippets/Trace/RequestTracerTest.php create mode 100644 tests/snippets/Trace/TraceContextTest.php create mode 100644 tests/unit/Trace/Reporter/EchoReporterTest.php create mode 100644 tests/unit/Trace/Reporter/FileReporterTest.php create mode 100644 tests/unit/Trace/Reporter/LoggerReporterTest.php create mode 100644 tests/unit/Trace/Reporter/SyncReporterTest.php create mode 100644 tests/unit/Trace/RequestHandlerTest.php create mode 100644 tests/unit/Trace/RequestTracerTest.php create mode 100644 tests/unit/Trace/Sampler/QpsSamplerTest.php create mode 100644 tests/unit/Trace/Sampler/RandomSamplerTest.php create mode 100644 tests/unit/Trace/Sampler/SamplerFactoryTest.php create mode 100644 tests/unit/Trace/TraceContextTest.php create mode 100644 tests/unit/Trace/Tracer/ContextTracerTest.php diff --git a/composer.json b/composer.json index 227ccc19b1f6..9f427e3a1339 100644 --- a/composer.json +++ b/composer.json @@ -46,7 +46,8 @@ "guzzlehttp/guzzle": "^5.3|^6.0", "guzzlehttp/psr7": "^1.2", "monolog/monolog": "~1", - "psr/http-message": "1.0.*" + "psr/http-message": "1.0.*", + "ramsey/uuid": "~3" }, "require-dev": { "phpunit/phpunit": "4.8.*", diff --git a/src/Trace/IdGeneratorTrait.php b/src/Trace/IdGeneratorTrait.php new file mode 100644 index 000000000000..581a2c643409 --- /dev/null +++ b/src/Trace/IdGeneratorTrait.php @@ -0,0 +1,36 @@ + 0.1]); // sample 0.1 requests per second +RequestTracer::start($reporter, ['sampler' => $sampler]); +``` + +Please note: While required for the `QpsSampler`, a PSR-6 implementation is +not included in this library. It will be necessary to include a separate +dependency to fulfill this requirement. For PSR-6 implementations, please see the +[Packagist PHP Package Repository](https://packagist.org/providers/psr/cache-implementation). +If the APCu extension is available (available on Google AppEngine Flexible Environment) +and you include the cache/apcu-adapter composer package, we will set up the cache for you. + +You can also choose to use the `RandomSampler` which simply samples a flat +percentage of requests. + +```php +use Google\Cloud\Trace\TraceClient; +use Google\Cloud\Trace\Reporter\SyncReporter; +use Google\Cloud\Trace\Sampler\RandomSampler; + +$trace = new TraceClient(); +$reporter = new SyncReporter($trace); +$sampler = new RandomSampler(0.1); // sample 10% of requests +RequestTracer::start($reporter, ['sampler' => $sampler]); +``` + +If you would like to provide your own sampler, create a class that implements `SamplerInterface`. + +## Tracing Code Blocks + +To add tracing to a block of code, you can use the closure/callable form or explicitly open +and close spans yourself. + +### Closure/Callable (preferred) + +```php +$pi = RequestTracer::inSpan(['name' => 'expensive-operation'], function() { + // some expensive operation + return calculatePi(1000); +}); + +$pi = RequestTracer::inSpan(['name' => 'expensive-operation'], 'calculatePi', [1000]); +``` + +### Explicit Span Management + +```php +RequestTracer::startSpan(['name' => 'expensive-operation']); +try { + $pi = calculatePi(1000); +} finally { + // Make sure we close the span to avoid mismatched span boundaries + RequestTracer::endSpan(); +} +``` diff --git a/src/Trace/Reporter/EchoReporter.php b/src/Trace/Reporter/EchoReporter.php new file mode 100644 index 000000000000..771b43b562c4 --- /dev/null +++ b/src/Trace/Reporter/EchoReporter.php @@ -0,0 +1,39 @@ +spans()); + return true; + } +} diff --git a/src/Trace/Reporter/FileReporter.php b/src/Trace/Reporter/FileReporter.php new file mode 100644 index 000000000000..1ead07c1722c --- /dev/null +++ b/src/Trace/Reporter/FileReporter.php @@ -0,0 +1,53 @@ +filename = $filename; + } + + /** + * Report the provided Trace to a backend. + * + * @param TracerInterface $tracer + * @return bool + */ + public function report(TracerInterface $tracer) + { + return file_put_contents($this->filename, json_encode($tracer->spans()) . PHP_EOL, FILE_APPEND) !== false; + } +} diff --git a/src/Trace/Reporter/LoggerReporter.php b/src/Trace/Reporter/LoggerReporter.php new file mode 100644 index 000000000000..95834526b369 --- /dev/null +++ b/src/Trace/Reporter/LoggerReporter.php @@ -0,0 +1,68 @@ +logger = $logger; + $this->level = $level; + } + + /** + * Report the provided Trace to a backend. + * + * @param TracerInterface $tracer + * @return bool + */ + public function report(TracerInterface $tracer) + { + try { + $this->logger->log($this->level, json_encode($tracer->spans())); + } catch (\Exception $e) { + return false; + } + return true; + } +} diff --git a/src/Trace/Reporter/NullReporter.php b/src/Trace/Reporter/NullReporter.php new file mode 100644 index 000000000000..30c6becd4276 --- /dev/null +++ b/src/Trace/Reporter/NullReporter.php @@ -0,0 +1,37 @@ +client = $client; + } + + /** + * Report the provided Trace to a backend. + * + * @param TracerInterface $tracer + * @return bool + */ + public function report(TracerInterface $tracer) + { + $spans = $tracer->spans(); + if (empty($spans)) { + return false; + } + + $trace = $this->client->trace($tracer->context()->traceId()); + $trace->setSpans($spans); + try { + return $this->client->insert($trace); + } catch (ServiceException $e) { + return false; + } + } +} diff --git a/src/Trace/RequestHandler.php b/src/Trace/RequestHandler.php new file mode 100644 index 000000000000..af9b50540f6e --- /dev/null +++ b/src/Trace/RequestHandler.php @@ -0,0 +1,259 @@ +reporter = $reporter; + $headers = $this->pluck('headers', $options, false) ?: $_SERVER; + $context = TraceContext::fromHeaders($headers); + + // If the context force disables tracing, don't consult the $sampler. + if ($context->enabled() !== false) { + $context->setEnabled($context->enabled() || $sampler->shouldSample()); + } + + // If the request was provided with a trace context header, we need to send it back with the response + // including whether the request was sampled or not. + if ($context->fromHeader()) { + $this->persistContextHeader($context); + } + + $this->tracer = $context->enabled() + ? new ContextTracer($context) + : new NullTracer(); + + $spanOptions = $options + [ + 'startTime' => $this->startTimeFromHeaders($headers), + 'name' => $this->nameFromHeaders($headers), + 'labels' => [] + ]; + $spanOptions['labels'] += $this->labelsFromHeaders($headers); + $this->tracer->startSpan($spanOptions); + + register_shutdown_function([$this, 'onExit']); + } + + /** + * The function registered as the shutdown function. Cleans up the trace and reports using the + * provided ReporterInterface. Adds additional labels to the root span detected from the response. + */ + public function onExit() + { + $responseCode = http_response_code(); + + // If a redirect, add the HTTP_REDIRECTED_URL label to the main span + if ($responseCode == 301 || $responseCode == 302) { + foreach (headers_list() as $header) { + if (substr($header, 0, 9) == 'Location:') { + $this->tracer->addRootLabel(self::HTTP_REDIRECTED_URL, substr($header, 10)); + break; + } + } + } + + $this->tracer->addRootLabel(self::HTTP_STATUS_CODE, $responseCode); + + // close all open spans + do { + $span = $this->tracer->endSpan(); + } while ($span); + $this->reporter->report($this->tracer); + } + + /** + * Return the tracer used for this request. + * + * @return TracerInterface + */ + public function tracer() + { + return $this->tracer; + } + + /** + * Instrument a callable by creating a TraceSpan that manages the startTime and endTime. + * If an exception is thrown while executing the callable, the exception will be caught, + * the span will be closed, and the exception will be re-thrown. + * + * @param array $spanOptions Options for the span. + * {@see Google\Cloud\Trace\TraceSpan::__construct()} + * @param callable $callable The callable to inSpan. + * @return mixed Returns whatever the callable returns + */ + public function inSpan(array $spanOptions, callable $callable, array $arguments = []) + { + return $this->tracer->inSpan($spanOptions, $callable, $arguments); + } + + /** + * Explicitly start a new TraceSpan. You will need to manage finishing the TraceSpan, + * including handling any thrown exceptions. + * + * @param array $spanOptions [optional] Options for the span. + * {@see Google\Cloud\Trace\TraceSpan::__construct()} + * @return TraceSpan + */ + public function startSpan(array $spanOptions = []) + { + return $this->tracer->startSpan($spanOptions); + } + + /** + * Explicitly finish the current context (TraceSpan). + * + * @return TraceSpan + */ + public function endSpan() + { + return $this->tracer->endSpan(); + } + + /** + * Return the current context (TraceSpan) + * + * @return TraceContext + */ + public function context() + { + return $this->tracer->context(); + } + + private function startTimeFromHeaders(array $headers) + { + return $this->detectKey(['REQUEST_TIME_FLOAT', 'REQUEST_TIME'], $headers); + } + + private function nameFromHeaders(array $headers) + { + if (array_key_exists('REQUEST_URI', $headers)) { + return $headers['REQUEST_URI']; + } + return self::DEFAULT_ROOT_SPAN_NAME; + } + + private function labelsFromHeaders(array $headers) + { + $labels = []; + + $labelMap = [ + self::HTTP_URL => ['REQUEST_URI'], + self::HTTP_METHOD => ['REQUEST_METHOD'], + self::HTTP_CLIENT_PROTOCOL => ['SERVER_PROTOCOL'], + self::HTTP_USER_AGENT => ['HTTP_USER_AGENT'], + self::HTTP_HOST => ['HTTP_HOST', 'SERVER_NAME'], + self::GAE_APP_MODULE => ['GAE_SERVICE'], + self::GAE_APP_MODULE_VERSION => ['GAE_VERSION'], + self::HTTP_CLIENT_CITY => ['HTTP_X_APPENGINE_CITY'], + self::HTTP_CLIENT_REGION => ['HTTP_X_APPENGINE_REGION'], + self::HTTP_CLIENT_COUNTRY => ['HTTP_X_APPENGINE_COUNTRY'] + ]; + foreach ($labelMap as $labelKey => $headerKeys) { + if ($val = $this->detectKey($headerKeys, $headers)) { + $labels[$labelKey] = $val; + } + } + + $labels[self::PID] = '' . getmypid(); + $labels[self::AGENT] = 'google-cloud-php ' . TraceClient::VERSION; + + return $labels; + } + + private function detectKey(array $keys, array $array) + { + foreach ($keys as $key) { + if (array_key_exists($key, $array)) { + return $array[$key]; + } + } + return null; + } + + private function persistContextHeader($context) + { + if (!headers_sent()) { + header('X-Cloud-Trace-Context: ' . $context); + } + } +} diff --git a/src/Trace/RequestTracer.php b/src/Trace/RequestTracer.php new file mode 100644 index 000000000000..cccb53c2b93b --- /dev/null +++ b/src/Trace/RequestTracer.php @@ -0,0 +1,216 @@ + 0.1]); + * RequestTracer::start($reporter, [ + * 'sampler' => $sampler + * ]); + * ``` + * + * The above uses a query-per-second sampler at 0.1 requests/second. The implementation + * requires a PSR-6 cache. See {@see Google\Cloud\Trace\Sampler\QpsSampler} for more information. + * You may provide your own implementation of {@see Google\Cloud\Trace\Sampler\SamplerInterface} + * or use one of the provided. You may provide a configuration array for the sampler instead. See + * {@see Google\Cloud\Trace\Sampler\SamplerFactory::build()} for builder options: + * + * ``` + * // $cache is a PSR-6 cache implementation + * RequestTracer::start($reporter, [ + * 'sampler' => [ + * 'type' => 'qps', + * 'rate' => 0.1, + * 'cache' => $cache + * ] + * ]); + * ``` + * + * To trace code, you can use static {@see Google\Cloud\Trace\RequestTracer::inSpan()} helper function: + * + * ``` + * RequestTracer::start($reporter); + * RequestTracer::inSpan(['name' => 'outer'], function () { + * // some code + * RequestTracer::inSpan(['name' => 'inner'], function () { + * // some code + * }); + * // some code + * }); + * ``` + * + * You can also start and finish spans independently throughout your code. + * + * Explicitly tracing spans: + * ``` + * RequestTracer::start($reporter); + * RequestTracer::startSpan(['name' => 'expensive-operation']); + * try { + * // do expensive operation + * } catch (\Exception $e) { + * RequestTracer::endSpan(); + * } + * ``` + * + * It is recommended that you use the {@see Google\Cloud\Trace\RequestTracer::inSpan()} + * method where you can. An uncaught exception between {@see Google\Cloud\Trace\RequestTracer::startSpan()} + * and {@see Google\Cloud\Trace\RequestTracer::endSpan()} may not correctly close spans. + */ +class RequestTracer +{ + /** + * @var RequestHandler Singleton instance + */ + private static $instance; + + /** + * Start a new trace session for this request. You should call this as early as + * possible for the most accurate results. + * + * @param ReporterInterface $reporter + * @param array $options { + * Configuration options. See + * {@see Google\Cloud\Trace\TraceSpan::__construct()} for the other available options. + * + * @type SamplerInterface|array $sampler Sampler or sampler factory build arguments. See + * {@see Google\Cloud\Trace\Sampler\SamplerFactory::build()} for the available options. + * @type array $headers Optional array of headers to use in place of $_SERVER + * } + * @return RequestHandler + */ + public static function start(ReporterInterface $reporter, array $options = []) + { + $samplerOptions = array_key_exists('sampler', $options) ? $options['sampler'] : []; + unset($options['sampler']); + + $sampler = ($samplerOptions instanceof SamplerInterface) + ? $samplerOptions + : SamplerFactory::build($samplerOptions); + + return self::$instance = new RequestHandler($reporter, $sampler, $options); + } + + /** + * Instrument a callable by creating a TraceSpan that manages the startTime and endTime. + * If an exception is thrown while executing the callable, the exception will be caught, + * the span will be closed, and the exception will be re-thrown. + * + * Example: + * ``` + * // Instrumenting code as a closure + * RequestTracer::inSpan(['name' => 'some-closure'], function () { + * // do something expensive + * }); + * ``` + * + * ``` + * // Instrumenting code as a callable (parameters optional) + * function fib($n) { + * // do something expensive + * } + * $number = RequestTracer::inSpan(['name' => 'some-callable'], 'fib', [10]); + * ``` + * + * @param array $spanOptions Options for the span. + * {@see Google\Cloud\Trace\TraceSpan::__construct()} + * @param callable $callable The callable to inSpan. + * @return mixed Returns whatever the callable returns + */ + public static function inSpan(array $spanOptions, callable $callable, array $arguments = []) + { + return self::$instance->inSpan($spanOptions, $callable, $arguments); + } + + /** + * Explicitly start a new TraceSpan. You will need to manage finishing the TraceSpan, + * including handling any thrown exceptions. + * + * Example: + * ``` + * RequestTracer::startSpan(['name' => 'expensive-operation']); + * try { + * // do something expensive + * } catch (\Exception $e) { + * RequestTracer::endSpan(); + * } + * ``` + * + * @param array $spanOptions [optional] Options for the span. + * {@see Google\Cloud\Trace\TraceSpan::__construct()} + */ + public static function startSpan(array $spanOptions = []) + { + return self::$instance->startSpan($spanOptions); + } + + /** + * Explicitly finish the current context (TraceSpan). + */ + public static function endSpan() + { + return self::$instance->endSpan(); + } + + /** + * Return the current context + * + * @return TraceContext + */ + public static function context() + { + return self::$instance->context(); + } + + /** + * Returns the RequestHandler instance + * + * @return RequestHandler + */ + public static function instance() + { + return self::$instance; + } +} diff --git a/src/Trace/Sampler/AlwaysOffSampler.php b/src/Trace/Sampler/AlwaysOffSampler.php new file mode 100644 index 000000000000..e3dc51898966 --- /dev/null +++ b/src/Trace/Sampler/AlwaysOffSampler.php @@ -0,0 +1,35 @@ +cache = $cache ?: $this->defaultCache(); + if (!$this->cache) { + throw new \InvalidArgumentException('Cannot use QpsSampler without providing a PSR-6 $cache'); + } + + $options += [ + 'cacheItemClass' => self::DEFAULT_CACHE_ITEM_CLASS, + 'rate' => self::DEFAULT_QPS_RATE, + 'key' => self::DEFAULT_CACHE_KEY + ]; + $this->cacheItemClass = $options['cacheItemClass']; + $this->rate = $options['rate']; + $this->key = $options['key']; + + if ($this->rate > 1 || $this->rate <= 0) { + throw new \InvalidArgumentException('QPS sampling rate must be less that 1 query per second'); + } + } + + /** + * Returns whether or not the request should be sampled. + * + * @return bool + */ + public function shouldSample() + { + // We will store the microtime timestamp in the cache because some + // cache implementations will not let you use expiry for anything less + // than 1 minute + if ($item = $this->cache->getItem($this->key)) { + if ((float) $item->get() > microtime(true)) { + return false; + } + } + + $item = new $this->cacheItemClass($this->key); + $item->set(microtime(true) + 1.0 / $this->rate); + + // TODO: what if the cache fails to save? + $this->cache->save($item); + + return true; + } + + /** + * Detect a usable PSR-6 cache implementation + * + * @return CacheItemPoolInterface + */ + private function defaultCache() + { + if (extension_loaded('apcu') && class_exists('\\Cache\\Adapter\\Apcu\\ApcuCachePool')) { + $this->cacheItemClass = \Cache\Adapter\Common\CacheItem::class; + return new \Cache\Adapter\Apcu\ApcuCachePool(); + } elseif (extension_loaded('apc') && class_exists('\\Cache\\Adapter\\Apc\\ApcCachePool')) { + $this->cacheItemClass = \Cache\Adapter\Common\CacheItem::class; + return new \Cache\Adapter\Apc\ApcCachePool(); + } + return null; + } + + /** + * Return the query-per-second rate + * + * @return float + */ + public function rate() + { + return $this->rate; + } +} diff --git a/src/Trace/Sampler/RandomSampler.php b/src/Trace/Sampler/RandomSampler.php new file mode 100644 index 000000000000..4b487152d119 --- /dev/null +++ b/src/Trace/Sampler/RandomSampler.php @@ -0,0 +1,64 @@ + 1 || $rate < 0) { + throw new \InvalidArgumentException('Percentage must be between 0 and 1'); + } + + $this->rate = $rate; + } + + /** + * Uses a pseudo-random number generator to decide if we should sample the request. + * + * @return bool + */ + public function shouldSample() + { + return lcg_value() <= $this->rate; + } + + /** + * Return the percentage of requests to sample represented as a float between 0 and 1 + * + * @return float + */ + public function rate() + { + return $this->rate; + } +} diff --git a/src/Trace/Sampler/SamplerFactory.php b/src/Trace/Sampler/SamplerFactory.php new file mode 100644 index 000000000000..28d07845fae5 --- /dev/null +++ b/src/Trace/Sampler/SamplerFactory.php @@ -0,0 +1,71 @@ + 'enabled', + 'rate' => 0.1, + 'cache' => null + ]; + + switch ($options['type']) { + case 'qps': + return new QpsSampler( + $options['cache'], + $options + ); + case 'random': + return new RandomSampler($options['rate']); + case 'enabled': + return new AlwaysOnSampler(); + case 'disabled': + default: + return new AlwaysOffSampler(); + } + } +} diff --git a/src/Trace/Sampler/SamplerInterface.php b/src/Trace/Sampler/SamplerInterface.php new file mode 100644 index 000000000000..77ea4c356000 --- /dev/null +++ b/src/Trace/Sampler/SamplerInterface.php @@ -0,0 +1,31 @@ +validateBatch($spans, TraceSpan::class); $this->spans = $spans; } - - /** - * Generates a random trace id as a UUID without dashes. - * - * @return string - */ - private function generateTraceId() - { - return sprintf( - '%04x%04x%04x%04x%04x%04x%04x%04x', - // 32 bits for "time_low" - mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - // 16 bits for "time_mid" - mt_rand(0, 0xffff), - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4 - mt_rand(0, 0x0fff) | 0x4000, - // 16 bits, 8 bits for "clk_seq_hi_res", - // 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1 - mt_rand(0, 0x3fff) | 0x8000, - // 48 bits for "node" - mt_rand(0, 0xffff), - mt_rand(0, 0xffff), - mt_rand(0, 0xffff) - ); - } } diff --git a/src/Trace/TraceContext.php b/src/Trace/TraceContext.php new file mode 100644 index 000000000000..43f0fb05726a --- /dev/null +++ b/src/Trace/TraceContext.php @@ -0,0 +1,166 @@ +traceId = $traceId ?: $this->generateTraceId(); + $this->spanId = $spanId; + $this->enabled = $enabled; + $this->fromHeader = $fromHeader; + } + + /** + * Fetch the current traceId. + * + * @return string + */ + public function traceId() + { + return $this->traceId; + } + + /** + * Fetch the current spanId. + * + * @return string + */ + public function spanId() + { + return $this->spanId; + } + + /** + * Set the current spanId. + * + * @param string|null $spanId The spanId to set. + */ + public function setSpanId($spanId) + { + $this->spanId = $spanId; + } + + /** + * Whether or not the request is being traced. + * + * @return bool + */ + public function enabled() + { + return $this->enabled; + } + + /** + * Set whether or not the request is being traced. + * + * @param bool|null $enabled + */ + public function setEnabled($enabled) + { + $this->enabled = $enabled; + } + + /** + * Whether or not this context was detected from a request header. + * + * @return bool + */ + public function fromHeader() + { + return $this->fromHeader; + } + + /** + * Returns a string form of the TraceContext. This is the format of the Trace Context Header + * and should be forwarded to downstream requests as the X-Cloud-Trace-Context header. + * + * @return string + */ + public function __toString() + { + $ret = '' . $this->traceId; + if ($this->spanId) { + $ret .= '/' . $this->spanId; + } + $ret .= ';o=' . ($this->enabled ? '1' : '0'); + return $ret; + } +} diff --git a/src/Trace/TraceSpan.php b/src/Trace/TraceSpan.php index a607564c48e3..664a5e882643 100644 --- a/src/Trace/TraceSpan.php +++ b/src/Trace/TraceSpan.php @@ -28,7 +28,7 @@ * for its suboperations. Spans do not need to be contiguous. There may be * gaps between spans in a trace. */ -class TraceSpan +class TraceSpan implements \JsonSerializable { use ArrayTrait; @@ -53,10 +53,12 @@ class TraceSpan * in a particular context. **Defaults to** * SPAN_KIND_UNSPECIFIED. * @type string $name The name of the span. - * @type string $startTime Start time of the span in - * nanoseconds in "Zulu" format. - * @type string $endTime End time of the span in - * nanoseconds in "Zulu" format. + * @type \DateTimeInterface|int|float|string $startTime Start time of the span in nanoseconds. + * If provided as a string, it must be in "Zulu" format. If provided as an int or float, it is + * expected to be a Unix timestamp. + * @type \DateTimeInterface|int|float|string $endTime End time of the span in nanoseconds. + * If provided as a string, it must be in "Zulu" format. If provided as an int or float, it is + * expected to be a Unix timestamp. * @type string $parentSpanId ID of the parent span if any. * @type array $labels Associative array of $label => $value * to attach to this span. @@ -65,9 +67,17 @@ class TraceSpan public function __construct($options = []) { $this->info = $this->pluckArray( - ['spanId', 'kind', 'name', 'startTime', 'endTime', 'parentSpanId', 'labels'], + ['spanId', 'kind', 'name', 'parentSpanId', 'labels'], $options ); + + if (array_key_exists('startTime', $options)) { + $this->setStart($options['startTime']); + } + if (array_key_exists('endTime', $options)) { + $this->setEnd($options['endTime']); + } + $this->info += [ 'kind' => self::SPAN_KIND_UNSPECIFIED ]; @@ -84,10 +94,11 @@ public function __construct($options = []) /** * Set the start time for this span. * - * @param \DateTimeInterface $when [optional] The start time of this span. - * **Defaults to** now. + * @param \DateTimeInterface|int|float|string $when [optional] The start time of this span. + * **Defaults to** now. If provided as a string, it must be in "Zulu" format. + * If provided as an int or float, it is expected to be a Unix timestamp. */ - public function setStart(\DateTimeInterface $when = null) + public function setStart($when = null) { $this->info['startTime'] = $this->formatDate($when); } @@ -95,10 +106,11 @@ public function setStart(\DateTimeInterface $when = null) /** * Set the end time for this span. * - * @param \DateTimeInterface $when [optional] The end time of this span. - * **Defaults to** now. + * @param \DateTimeInterface|int|float|string $when [optional] The end time of this span. + * **Defaults to** now. If provided as a string, it must be in "Zulu" format. + * If provided as an int or float, it is expected to be a Unix timestamp. */ - public function setEnd(\DateTimeInterface $when = null) + public function setEnd($when = null) { $this->info['endTime'] = $this->formatDate($when); } @@ -113,6 +125,18 @@ public function spanId() return $this->info['spanId']; } + /** + * Retrieve the ID of this span's parent if it exists. + * + * @return string + */ + public function parentSpanId() + { + return array_key_exists('parentSpanId', $this->info) + ? $this->info['parentSpanId'] + : null; + } + /** * Retrieve the name of this span. * @@ -133,6 +157,16 @@ public function info() return $this->info; } + /** + * Returns the info array for serialization. + * + * @return array + */ + public function jsonSerialize() + { + return $this->info; + } + /** * Attach labels to this span. * @@ -160,19 +194,25 @@ public function addLabel($label, $value) } /** - * Returns a "Zulu" formatted string representing the - * provided \DateTime. + * Returns a "Zulu" formatted string representing the provided \DateTime. * - * @param \DateTimeInterface $when [optional] The end time of this span. - * **Defaults to** now. + * @param \DateTimeInterface|int|float|string $when [optional] The end time of this span. + * **Defaults to** now. If provided as a string, it must be in "Zulu" format. + * If provided as an int or float, it is expected to be a Unix timestamp. * @return string */ - private function formatDate(\DateTimeInterface $when = null) + private function formatDate($when = null) { - if (!$when) { + if (is_string($when)) { + return $when; + } elseif (!$when) { list($usec, $sec) = explode(' ', microtime()); $micro = sprintf("%06d", $usec * 1000000); $when = new \DateTime(date('Y-m-d H:i:s.' . $micro)); + } elseif (is_numeric($when)) { + // Expect that this is a timestamp + $micro = sprintf("%06d", ($when - floor($when)) * 1000000); + $when = new \DateTime(date('Y-m-d H:i:s.'. $micro, (int) $when)); } $when->setTimezone(new \DateTimeZone('UTC')); return $when->format('Y-m-d\TH:i:s.u000\Z'); diff --git a/src/Trace/Tracer/ContextTracer.php b/src/Trace/Tracer/ContextTracer.php new file mode 100644 index 000000000000..f12614dcdf62 --- /dev/null +++ b/src/Trace/Tracer/ContextTracer.php @@ -0,0 +1,169 @@ +context = $context ?: new TraceContext(); + } + + /** + * Instrument a callable by creating a Span that manages the startTime and endTime. + * + * @param array $spanOptions Options for the span. + * {@see Google\Cloud\Trace\TraceSpan::__construct()} + * @param callable $callable The callable to inSpan. + * @param array $arguments [optional] Arguments for the callable. + * @return mixed The result of the callable + */ + public function inSpan(array $spanOptions, callable $callable, array $arguments = []) + { + $this->startSpan($spanOptions); + try { + return call_user_func_array($callable, $arguments); + } finally { + $this->endSpan(); + } + } + + /** + * Start a new Span. The start time is already set to the current time. + * + * @param array $spanOptions [optional] Options for the span. + * {@see Google\Cloud\Trace\TraceSpan::__construct()} + */ + public function startSpan(array $spanOptions = []) + { + $spanOptions += [ + 'parentSpanId' => $this->context()->spanId(), + 'startTime' => microtime(true) + ]; + + $span = new TraceSpan($spanOptions); + array_push($this->spans, $span); + array_unshift($this->stack, $span); + $this->context->setSpanId($span->spanId()); + } + + /** + * Finish the current context's Span. + * + * @return bool + */ + public function endSpan() + { + $span = array_shift($this->stack); + $this->context->setSpanId(empty($this->stack) ? null : $this->stack[0]->spanId()); + if ($span) { + $span->setEnd(); + return true; + } + return false; + } + + /** + * Return the current context. + * + * @return TraceContext + */ + public function context() + { + return $this->context; + } + + /** + * Return the spans collected. + * + * @return TraceSpan[] + */ + public function spans() + { + return $this->spans; + } + + /** + * Add a label to the current TraceSpan + * + * @param string $label + * @param string $value + */ + public function addLabel($label, $value) + { + if (!empty($this->stack)) { + $this->stack[0]->addLabel($label, $value); + } + } + + /** + * Add a label to the primary TraceSpan + * + * @param string $label + * @param string $value + */ + public function addRootLabel($label, $value) + { + if (!empty($this->spans)) { + $this->spans[0]->addLabel($label, $value); + } + } + + /** + * Whether or not this tracer is enabled. + * + * @return bool + */ + public function enabled() + { + return $this->context->enabled(); + } +} diff --git a/src/Trace/Tracer/NullTracer.php b/src/Trace/Tracer/NullTracer.php new file mode 100644 index 000000000000..263daadf237e --- /dev/null +++ b/src/Trace/Tracer/NullTracer.php @@ -0,0 +1,109 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->client = \Google\Cloud\Dev\stub(TraceClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(RequestTracer::class); + $snippet->invoke(); + $handler = RequestTracer::instance(); + + $this->assertInstanceOf(RequestHandler::class, $handler); + $this->assertCount(1, $handler->tracer()->spans()); + } + + public function testClassApcu() + { + $snippet = $this->snippetFromClass(RequestTracer::class, 1); + $snippet->addUse(RequestTracer::class); + $snippet->addUse(QpsSampler::class); + $cache = $this->prophesize(CacheItemPoolInterface::class); + $snippet->addLocal('cache', $cache->reveal()); + $reporter = $this->prophesize(ReporterInterface::class); + $snippet->addLocal('reporter', $reporter->reveal()); + + $res = $snippet->invoke(); + $handler = RequestTracer::instance(); + + $this->assertInstanceOf(RequestHandler::class, $handler); + $this->assertCount(1, $handler->tracer()->spans()); + } + + public function testClassApcuFactory() + { + $snippet = $this->snippetFromClass(RequestTracer::class, 2); + $snippet->addUse(RequestTracer::class); + $cache = $this->prophesize(CacheItemPoolInterface::class); + $snippet->addLocal('cache', $cache->reveal()); + $reporter = $this->prophesize(ReporterInterface::class); + $snippet->addLocal('reporter', $reporter->reveal()); + + $res = $snippet->invoke(); + $handler = RequestTracer::instance(); + + $this->assertInstanceOf(RequestHandler::class, $handler); + $this->assertCount(1, $handler->tracer()->spans()); + } + + public function testNestedSpans() + { + $snippet = $this->snippetFromClass(RequestTracer::class, 3); + $snippet->addUse(RequestTracer::class); + $reporter = $this->prophesize(ReporterInterface::class); + $snippet->addLocal('reporter', $reporter->reveal()); + + $res = $snippet->invoke(); + $handler = RequestTracer::instance(); + + $this->assertInstanceOf(RequestHandler::class, $handler); + $this->assertEquals(3, count($handler->tracer()->spans())); + } + + public function testExplicitSpans() + { + $snippet = $this->snippetFromClass(RequestTracer::class, 4); + $snippet->addUse(RequestTracer::class); + $reporter = $this->prophesize(ReporterInterface::class); + $snippet->addLocal('reporter', $reporter->reveal()); + + $res = $snippet->invoke(); + $handler = RequestTracer::instance(); + + $this->assertInstanceOf(RequestHandler::class, $handler); + $this->assertEquals(2, count($handler->tracer()->spans())); + } + + public function testInSpanClosure() + { + $snippet = $this->snippetFromMethod(RequestTracer::class, 'inSpan'); + $snippet->addUse(RequestTracer::class); + $snippet->invoke(); + + $spans = RequestTracer::instance()->tracer()->spans(); + $lastSpan = $spans[count($spans) - 1]; + $this->assertEquals('some-closure', $lastSpan->name()); + } + + public function testInSpanCallable() + { + $snippet = $this->snippetFromMethod(RequestTracer::class, 'inSpan', 1); + $snippet->addUse(RequestTracer::class); + $snippet->invoke(); + + $spans = RequestTracer::instance()->tracer()->spans(); + $lastSpan = $spans[count($spans) - 1]; + $this->assertEquals('some-callable', $lastSpan->name()); + } + + public function testStartSpan() + { + $snippet = $this->snippetFromMethod(RequestTracer::class, 'startSpan'); + $snippet->addUse(RequestTracer::class); + $snippet->invoke(); + + $spans = RequestTracer::instance()->tracer()->spans(); + $lastSpan = $spans[count($spans) - 1]; + $this->assertEquals('expensive-operation', $lastSpan->name()); + } +} diff --git a/tests/snippets/Trace/TraceClientTest.php b/tests/snippets/Trace/TraceClientTest.php index e38d76260b43..c845dfb7665a 100644 --- a/tests/snippets/Trace/TraceClientTest.php +++ b/tests/snippets/Trace/TraceClientTest.php @@ -27,8 +27,6 @@ */ class TraceClientTest extends SnippetTestCase { - const BUCKET = 'my-bucket'; - private $connection; private $client; diff --git a/tests/snippets/Trace/TraceContextTest.php b/tests/snippets/Trace/TraceContextTest.php new file mode 100644 index 000000000000..0c72dc6bf57a --- /dev/null +++ b/tests/snippets/Trace/TraceContextTest.php @@ -0,0 +1,36 @@ +snippetFromClass(TraceContext::class); + $res = $snippet->invoke('context'); + + $this->assertInstanceOf(TraceContext::class, $res->returnVal()); + } +} diff --git a/tests/unit/Trace/Reporter/EchoReporterTest.php b/tests/unit/Trace/Reporter/EchoReporterTest.php new file mode 100644 index 000000000000..64db8f4f4c42 --- /dev/null +++ b/tests/unit/Trace/Reporter/EchoReporterTest.php @@ -0,0 +1,56 @@ +tracer = $this->prophesize(TracerInterface::class); + } + + public function testLogsTrace() + { + $spans = [ + new TraceSpan([ + 'name' => 'span', + 'startTime' => microtime(true), + 'endTime' => microtime(true) + 10 + ]) + ]; + $this->tracer->context()->willReturn(new TraceContext('testtraceid')); + $this->tracer->spans()->willReturn($spans); + + ob_start(); + $reporter = new EchoReporter(); + $this->assertTrue($reporter->report($this->tracer->reveal())); + $output = ob_get_contents(); + ob_end_clean(); + $this->assertGreaterThan(0, strlen($output)); + } +} diff --git a/tests/unit/Trace/Reporter/FileReporterTest.php b/tests/unit/Trace/Reporter/FileReporterTest.php new file mode 100644 index 000000000000..ddb5e6524cc0 --- /dev/null +++ b/tests/unit/Trace/Reporter/FileReporterTest.php @@ -0,0 +1,60 @@ +filename = tempnam(sys_get_temp_dir(), 'traces'); + $this->tracer = $this->prophesize(TracerInterface::class); + } + + public function tearDown() + { + @unlink($this->filename); + } + + public function testLogsTrace() + { + $spans = [ + new TraceSpan([ + 'name' => 'span', + 'startTime' => microtime(true), + 'endTime' => microtime(true) + 10 + ]) + ]; + $this->tracer->context()->willReturn(new TraceContext('testtraceid')); + $this->tracer->spans()->willReturn($spans); + + $reporter = new FileReporter($this->filename); + $this->assertTrue($reporter->report($this->tracer->reveal())); + $this->assertGreaterThan(0, strlen(@file_get_contents($this->filename))); + } +} diff --git a/tests/unit/Trace/Reporter/LoggerReporterTest.php b/tests/unit/Trace/Reporter/LoggerReporterTest.php new file mode 100644 index 000000000000..36be7b867b05 --- /dev/null +++ b/tests/unit/Trace/Reporter/LoggerReporterTest.php @@ -0,0 +1,58 @@ +logger = $this->prophesize(LoggerInterface::class); + $this->tracer = $this->prophesize(TracerInterface::class); + } + + public function testLogsTrace() + { + $spans = [ + new TraceSpan([ + 'name' => 'span', + 'startTime' => microtime(true), + 'endTime' => microtime(true) + 10 + ]) + ]; + $this->tracer->context()->willReturn(new TraceContext('testtraceid')); + $this->tracer->spans()->willReturn($spans); + + $this->logger->log('some-level', Argument::type('string'))->shouldBeCalled(); + + $reporter = new LoggerReporter($this->logger->reveal(), 'some-level'); + $this->assertTrue($reporter->report($this->tracer->reveal())); + } +} diff --git a/tests/unit/Trace/Reporter/SyncReporterTest.php b/tests/unit/Trace/Reporter/SyncReporterTest.php new file mode 100644 index 000000000000..44a6e16a262e --- /dev/null +++ b/tests/unit/Trace/Reporter/SyncReporterTest.php @@ -0,0 +1,98 @@ +client = new TraceTestClient(['projectId' => 'project']); + $this->tracer = $this->prophesize(TracerInterface::class); + $this->connection = $this->prophesize(ConnectionInterface::class); + } + + public function testReportsTrace() + { + $spans = [ + new TraceSpan([ + 'name' => 'span', + 'startTime' => microtime(true), + 'endTime' => microtime(true) + 10 + ]) + ]; + $this->tracer->context()->willReturn(new TraceContext('testtraceid')); + $this->tracer->spans()->willReturn($spans); + + $this->connection->patchTraces(Argument::any())->willReturn(true); + $this->client->setConnection($this->connection->reveal()); + + $reporter = new SyncReporter($this->client); + $this->assertTrue($reporter->report($this->tracer->reveal())); + } + + public function testSkipsReportingWhenNoSpans() + { + $this->tracer->spans()->willReturn([]); + + $reporter = new SyncReporter($this->client); + $this->assertFalse($reporter->report($this->tracer->reveal())); + } + + public function testHandlesServiceFailure() + { + $spans = [ + new TraceSpan([ + 'name' => 'span', + 'startTime' => microtime(true), + 'endTime' => microtime(true) + 10 + ]) + ]; + $this->tracer->context()->willReturn(new TraceContext('testtraceid')); + $this->tracer->spans()->willReturn($spans); + + $this->connection->patchTraces(Argument::any())->willThrow(new ServiceException('An error occurred')); + $this->client->setConnection($this->connection->reveal()); + + $reporter = new SyncReporter($this->client); + $this->assertFalse($reporter->report($this->tracer->reveal())); + } +} + +class TraceTestClient extends TraceClient +{ + public function setConnection($connection) + { + $this->connection = $connection; + } +} diff --git a/tests/unit/Trace/RequestHandlerTest.php b/tests/unit/Trace/RequestHandlerTest.php new file mode 100644 index 000000000000..886ec033c55e --- /dev/null +++ b/tests/unit/Trace/RequestHandlerTest.php @@ -0,0 +1,155 @@ +reporter = $this->prophesize(ReporterInterface::class); + $this->sampler = $this->prophesize(SamplerInterface::class); + } + + public function testCanTrackContext() + { + $this->sampler->shouldSample()->willReturn(true); + + $rt = new RequestHandler( + $this->reporter->reveal(), + $this->sampler->reveal() + ); + $rt->inSpan(['name' => 'inner'], function () {}); + $rt->onExit(); + $spans = $rt->tracer()->spans(); + $this->assertCount(2, $spans); + foreach ($spans as $span) { + $this->assertInstanceOf(TraceSpan::class, $span); + $this->assertArrayHasKey('endTime', $span->info()); + } + $this->assertEquals('main', $spans[0]->name()); + $this->assertEquals('inner', $spans[1]->name()); + $this->assertEquals($spans[0]->spanId(), $spans[1]->info()['parentSpanId']); + } + + public function testCanParseLabels() + { + $this->sampler->shouldSample()->willReturn(true); + + $rt = new RequestHandler( + $this->reporter->reveal(), + $this->sampler->reveal(), + [ + 'headers' => [ + 'REQUEST_URI' => '/some/uri', + 'REQUEST_METHOD' => 'POST', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'HTTP_USER_AGENT' => 'test agent 0.1', + 'HTTP_HOST' => 'example.com:8080', + 'GAE_SERVICE' => 'test_app', + 'GAE_VERSION' => 'some_version' + ] + ] + ); + $span = $rt->tracer()->spans()[0]; + $labels = $span->info()['labels']; + $expectedLabels = [ + '/http/url' => '/some/uri', + '/http/method' => 'POST', + '/http/client_protocol' => 'HTTP/1.1', + '/http/user_agent' => 'test agent 0.1', + '/http/host' => 'example.com:8080', + 'g.co/gae/app/module' => 'test_app', + 'g.co/gae/app/module_version' => 'some_version' + ]; + + foreach ($expectedLabels as $key => $value) { + $this->assertArrayHasKey($key, $labels); + $this->assertEquals($value, $labels[$key]); + } + $this->assertArrayHasKey('/pid', $labels); + $this->assertArrayHasKey('/agent', $labels); + $this->assertEquals('google-cloud-php 0.1.0', $labels['/agent']); + } + + public function testCanParseParentContext() + { + $rt = new RequestHandler( + $this->reporter->reveal(), + $this->sampler->reveal(), + [ + 'headers' => [ + 'HTTP_X_CLOUD_TRACE_CONTEXT' => '12345678901234567890123456789012/5555;o=1' + ] + ] + ); + $span = $rt->tracer()->spans()[0]; + $this->assertEquals('5555', $span->info()['parentSpanId']); + $context = $rt->context(); + $this->assertEquals('12345678901234567890123456789012', $context->traceId()); + } + + public function testForceEnabledContextHeader() + { + $rt = new RequestHandler( + $this->reporter->reveal(), + $this->sampler->reveal(), + [ + 'headers' => [ + 'HTTP_X_CLOUD_TRACE_CONTEXT' => '12345678901234567890123456789012;o=1' + ] + ] + ); + $tracer = $rt->tracer(); + + $this->assertTrue($tracer->enabled()); + } + + public function testForceDisabledContextHeader() + { + $rt = new RequestHandler( + $this->reporter->reveal(), + $this->sampler->reveal(), + [ + 'headers' => [ + 'HTTP_X_CLOUD_TRACE_CONTEXT' => '12345678901234567890123456789012;o=0' + ] + ] + ); + $tracer = $rt->tracer(); + + $this->assertFalse($tracer->enabled()); + $this->assertInstanceOf(NullTracer::class, $tracer); + } + +} diff --git a/tests/unit/Trace/RequestTracerTest.php b/tests/unit/Trace/RequestTracerTest.php new file mode 100644 index 000000000000..48d9858fb666 --- /dev/null +++ b/tests/unit/Trace/RequestTracerTest.php @@ -0,0 +1,56 @@ +reporter = $this->prophesize(ReporterInterface::class); + } + + public function testForceDisabled() + { + $rt = RequestTracer::start($this->reporter->reveal(), [ + 'sampler' => ['type' => 'disabled'] + ]); + $tracer = $rt->tracer(); + + $this->assertFalse($tracer->enabled()); + $this->assertInstanceOf(NullTracer::class, $tracer); + } + + public function testForceEnabled() + { + $rt = RequestTracer::start($this->reporter->reveal(), [ + 'sampler' => ['type' => 'enabled'] + ]); + $tracer = $rt->tracer(); + + $this->assertTrue($tracer->enabled()); + } +} diff --git a/tests/unit/Trace/Sampler/QpsSamplerTest.php b/tests/unit/Trace/Sampler/QpsSamplerTest.php new file mode 100644 index 000000000000..98fa19adb235 --- /dev/null +++ b/tests/unit/Trace/Sampler/QpsSamplerTest.php @@ -0,0 +1,84 @@ +prophesize(CacheItemInterface::class); + $item->get()->willReturn(microtime(true) + 100); + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem(Argument::any())->willReturn($item->reveal()); + + $sampler = new QpsSampler($cache->reveal()); + $this->assertFalse($sampler->shouldSample()); + } + + public function testCachedValueExpired() + { + $item = $this->prophesize(CacheItemInterface::class); + $item->get()->willReturn(microtime(true) - 100); + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem(Argument::any())->willReturn($item->reveal()); + $cache->save(Argument::any())->willReturn(true); + + $sampler = new QpsSampler($cache->reveal()); + $this->assertTrue($sampler->shouldSample()); + } + + public function testNotCached() + { + $cache = $this->prophesize(CacheItemPoolInterface::class); + $cache->getItem(Argument::any())->willReturn(null); + $cache->save(Argument::any())->willReturn(true); + + $sampler = new QpsSampler($cache->reveal()); + $this->assertTrue($sampler->shouldSample()); + } + + /** + * @dataProvider invalidRates + * @expectedException \InvalidArgumentException + */ + public function testInvalidRate($rate) + { + $cache = $this->prophesize(CacheItemPoolInterface::class); + $sampler = new QpsSampler($cache->reveal(), [ + 'rate' => $rate + ]); + } + + public function invalidRates() + { + return [ + [0], + [-1], + [10], + [1.1] + ]; + } +} diff --git a/tests/unit/Trace/Sampler/RandomSamplerTest.php b/tests/unit/Trace/Sampler/RandomSamplerTest.php new file mode 100644 index 000000000000..fb5d3706d9dc --- /dev/null +++ b/tests/unit/Trace/Sampler/RandomSamplerTest.php @@ -0,0 +1,44 @@ +assertTrue($sampler->shouldSample()); + $this->assertInstanceOf(AlwaysOnSampler::class, $sampler); + } + + public function testBuildQps() + { + $cache = $this->prophesize(CacheItemPoolInterface::class); + + $sampler = SamplerFactory::build([ + 'type' => 'qps', + 'rate' => 0.2, + 'cache' => $cache->reveal() + ]); + $this->assertInstanceOf(QpsSampler::class, $sampler); + $this->assertEquals(0.2, $sampler->rate()); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testQpsRequiresCache() + { + SamplerFactory::build([ + 'type' => 'qps' + ]); + } + + public function testQpsDefaultRate() + { + $cache = $this->prophesize(CacheItemPoolInterface::class); + + $sampler = SamplerFactory::build([ + 'type' => 'qps', + 'cache' => $cache->reveal() + ]); + $this->assertInstanceOf(QpsSampler::class, $sampler); + $this->assertEquals(0.1, $sampler->rate()); + } + + public function testBuildRandom() + { + $sampler = SamplerFactory::build([ + 'type' => 'random', + 'rate' => 0.2 + ]); + $this->assertInstanceOf(RandomSampler::class, $sampler); + $this->assertEquals(0.2, $sampler->rate()); + } + + public function testRandomDefaultRate() + { + $sampler = SamplerFactory::build([ + 'type' => 'random' + ]); + $this->assertInstanceOf(RandomSampler::class, $sampler); + $this->assertEquals(0.1, $sampler->rate()); + } + +} diff --git a/tests/unit/Trace/TraceContextTest.php b/tests/unit/Trace/TraceContextTest.php new file mode 100644 index 000000000000..0fad30af47e5 --- /dev/null +++ b/tests/unit/Trace/TraceContextTest.php @@ -0,0 +1,57 @@ + $header]); + $this->assertEquals($traceId, $context->traceId()); + $this->assertEquals($spanId, $context->spanId()); + $this->assertEquals($enabled, $context->enabled()); + $this->assertTrue($context->fromHeader()); + } + + /** + * @dataProvider traceHeaders + */ + public function testToString($traceId, $spanId, $enabled, $expected) + { + $context = new TraceContext($traceId, $spanId, $enabled); + $this->assertEquals($expected, (string) $context); + } + + public function traceHeaders() + { + return [ + ['123456789012345678901234567890ab', '1234', false, '123456789012345678901234567890ab/1234;o=0'], + ['123456789012345678901234567890ab', '1234', true, '123456789012345678901234567890ab/1234;o=1'], + ['123456789012345678901234567890ab', null, false, '123456789012345678901234567890ab;o=0'], + ['123456789012345678901234567890ab', null, true, '123456789012345678901234567890ab;o=1'], + ]; + } +} diff --git a/tests/unit/Trace/TraceSpanTest.php b/tests/unit/Trace/TraceSpanTest.php index 67af0e6abf9c..349ecec99c11 100644 --- a/tests/unit/Trace/TraceSpanTest.php +++ b/tests/unit/Trace/TraceSpanTest.php @@ -124,4 +124,25 @@ public function testIgnoresUnknownFields() $info = $traceSpan->info(); $this->assertArrayNotHasKey('extravalue', $info); } + + /** + * @dataProvider timestampFields + */ + public function testCanFormatTimestamps($field, $timestamp, $expected) + { + $traceSpan = new TraceSpan([$field => $timestamp]); + $this->assertEquals($expected, $traceSpan->info()[$field]); + } + + public function timestampFields() + { + return [ + ['startTime', 1490737410, '2017-03-28T21:43:30.000000000Z'], + ['startTime', 1490737450.4843, '2017-03-28T21:44:10.484299000Z'], + ['startTime', '2017-03-28T21:44:10.484299000Z', '2017-03-28T21:44:10.484299000Z'], + ['endTime', 1490737410, '2017-03-28T21:43:30.000000000Z'], + ['endTime', 1490737450.4843, '2017-03-28T21:44:10.484299000Z'], + ['endTime', '2017-03-28T21:44:10.484299000Z', '2017-03-28T21:44:10.484299000Z'], + ]; + } } diff --git a/tests/unit/Trace/TraceTest.php b/tests/unit/Trace/TraceTest.php index da0d1e48a0a0..828267cd828f 100644 --- a/tests/unit/Trace/TraceTest.php +++ b/tests/unit/Trace/TraceTest.php @@ -45,8 +45,8 @@ public function testLoadFromArray() 'spanId' => '12345', 'kind' => 'SPAN_KIND_UNSPECIFIED', 'name' => 'spanname', - 'startTime' => '', - 'endTime' => '' + 'startTime' => '2017-03-28T21:44:10.484299000Z', + 'endTime' => '2017-03-28T21:44:11.123456000Z' ] ] ); diff --git a/tests/unit/Trace/Tracer/ContextTracerTest.php b/tests/unit/Trace/Tracer/ContextTracerTest.php new file mode 100644 index 000000000000..b214f2a18c23 --- /dev/null +++ b/tests/unit/Trace/Tracer/ContextTracerTest.php @@ -0,0 +1,83 @@ +context(); + + $this->assertEquals('traceid', $context->traceId()); + $this->assertEquals('spanid', $context->spanId()); + + $tracer->inSpan(['name' => 'test'], function() use ($tracer) { + $context = $tracer->context(); + $this->assertNotEquals('spanid', $context->spanId()); + }); + + $spans = $tracer->spans(); + $this->assertCount(1, $spans); + $span = $spans[0]; + $this->assertEquals('test', $span->name()); + $this->assertEquals('spanid', $span->parentSpanId()); + } + + public function testAddsLabelsToCurrentSpan() + { + $tracer = new ContextTracer(); + $tracer->startSpan(['name' => 'root']); + $tracer->startSpan(['name' => 'inner']); + $tracer->addLabel('foo', 'bar'); + $tracer->endSpan(); + $tracer->endSpan(); + + $spans = $tracer->spans(); + $this->assertCount(2, $spans); + $span = $spans[1]; + $this->assertEquals('inner', $span->name()); + $info = $span->info(); + $this->assertEquals('bar', $info['labels']['foo']); + } + + public function testAddsLabelsToRootSpan() + { + $tracer = new ContextTracer(); + $tracer->startSpan(['name' => 'root']); + $tracer->startSpan(['name' => 'inner']); + $tracer->addRootLabel('foo', 'bar'); + $tracer->endSpan(); + $tracer->endSpan(); + + $spans = $tracer->spans(); + $this->assertCount(2, $spans); + $span = $spans[0]; + $this->assertEquals('root', $span->name()); + $info = $span->info(); + $this->assertEquals('bar', $info['labels']['foo']); + } +} From ecae3e8f36637ef4eb8603e482f8b6a417d7fb48 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 25 May 2017 10:16:50 -0400 Subject: [PATCH 32/46] Update required symfony/lock dependency version (#515) --- composer.json | 2 +- src/Core/composer.json | 2 +- src/Spanner/Session/CacheSessionPool.php | 4 ++-- src/Spanner/composer.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 9f427e3a1339..1945bfc2a393 100644 --- a/composer.json +++ b/composer.json @@ -59,7 +59,7 @@ "vierbergenlars/php-semver": "^3.0", "google/proto-client-php": "^0.13", "google/gax": "^0.9", - "symfony/lock": "dev-master#1ba6ac9" + "symfony/lock": "3.3.x-dev#1ba6ac9" }, "suggest": { "google/gax": "Required to support gRPC", diff --git a/src/Core/composer.json b/src/Core/composer.json index 6072c3671b46..d7b094add539 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -13,7 +13,7 @@ "psr/http-message": "1.0.*" }, "suggest": { - "symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: dev-master#1ba6ac9" + "symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: 3.3.x-dev#1ba6ac9" }, "extra": { "component": { diff --git a/src/Spanner/Session/CacheSessionPool.php b/src/Spanner/Session/CacheSessionPool.php index 229d299404b9..90c1923e3f15 100644 --- a/src/Spanner/Session/CacheSessionPool.php +++ b/src/Spanner/Session/CacheSessionPool.php @@ -67,7 +67,7 @@ * also required to be installed as a separate dependency. In our current alpha * state with Spanner we are relying on the following dev commit: * - * `composer require symfony/lock:dev-master#1ba6ac9` + * `composer require symfony/lock:3.3.x-dev#1ba6ac9` * * As development continues, this dependency on a dev-master branch will be * discontinued. Please also note, since this is a dev-master dependency it may @@ -754,7 +754,7 @@ private function getDefaultLock() throw new \RuntimeException( 'The symfony/lock component must be installed in order for ' . 'a default lock to be assumed. Please run the following from ' . - 'the command line: composer require symfony/lock:dev-master#1ba6ac9. ' . + 'the command line: composer require symfony/lock:3.3.x-dev#1ba6ac9. ' . 'Please note, since this is a dev-master dependency it may ' . 'require modifications to your composer minimum-stability ' . 'settings.' diff --git a/src/Spanner/composer.json b/src/Spanner/composer.json index 8c8749e150ce..b9ff6d27761c 100644 --- a/src/Spanner/composer.json +++ b/src/Spanner/composer.json @@ -10,7 +10,7 @@ "google/proto-client-php": "^0.13" }, "suggest": { - "symfony/lock": "Required for the default session handler. Should be included as follows: symfony/lock:dev-master#1ba6ac9" + "symfony/lock": "Required for the default session handler. Should be included as follows: 3.3.x-dev#1ba6ac9" }, "extra": { "component": { From ae772dbcc35131b2f44e039e716be81b5b0893b0 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 25 May 2017 11:14:08 -0400 Subject: [PATCH 33/46] Test component composer integration, bump gax requirement (#507) * Test component composer integeration, bump gax requirement * Use dry-run and repository path * Switch to exec() * Add to travis build * Skip if grpc missing --- .travis.yml | 1 + dev/sh/test-composer | 5 ++ dev/sh/tests | 6 +- src/ErrorReporting/composer.json | 3 +- src/Monitoring/composer.json | 3 +- tests/component/TestComposerInstall.php | 76 +++++++++++++++++++++++++ 6 files changed, 87 insertions(+), 7 deletions(-) create mode 100755 dev/sh/test-composer create mode 100644 tests/component/TestComposerInstall.php diff --git a/.travis.yml b/.travis.yml index 0c37a6d81b32..ed157242affc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ before_script: script: - ./dev/sh/tests + - ./dev/sh/test-composer - vendor/bin/phpcs --standard=./phpcs-ruleset.xml - ./dev/sh/build-docs diff --git a/dev/sh/test-composer b/dev/sh/test-composer new file mode 100755 index 000000000000..12a1fb9b3f68 --- /dev/null +++ b/dev/sh/test-composer @@ -0,0 +1,5 @@ +#!/bin/bash + +echo "Checking Component Installability" + +php $(dirname $0)/../../tests/component/TestComposerInstall.php diff --git a/dev/sh/tests b/dev/sh/tests index fa7856223e53..1d512797d3d8 100755 --- a/dev/sh/tests +++ b/dev/sh/tests @@ -5,19 +5,19 @@ set -e function unit () { echo "Running Unit Test Suite" - vendor/bin/phpunit + $(dirname $0)/../../vendor/bin/phpunit } function system () { echo "Running System Test Suite" - vendor/bin/phpunit -c phpunit-system.xml.dist + $(dirname $0)/../../vendor/bin/phpunit -c phpunit-system.xml.dist } function snippets() { echo "Running Snippet Test Suite" - vendor/bin/phpunit -c phpunit-snippets.xml.dist + $(dirname $0)/../../vendor/bin/phpunit -c phpunit-snippets.xml.dist } unit diff --git a/src/ErrorReporting/composer.json b/src/ErrorReporting/composer.json index a66d81853091..96a2fdb0fcce 100644 --- a/src/ErrorReporting/composer.json +++ b/src/ErrorReporting/composer.json @@ -5,9 +5,8 @@ "minimum-stability": "stable", "require": { "ext-grpc": "*", - "google/cloud-core": "^1.0", "google/proto-client-php": "^0.13", - "google/gax": "^0.8" + "google/gax": "^0.9" }, "extra": { "component": { diff --git a/src/Monitoring/composer.json b/src/Monitoring/composer.json index ef8c2abc87ff..1343c2173513 100644 --- a/src/Monitoring/composer.json +++ b/src/Monitoring/composer.json @@ -5,9 +5,8 @@ "minimum-stability": "stable", "require": { "ext-grpc": "*", - "google/cloud-core": "^1.0", "google/proto-client-php": "^0.13", - "google/gax": "^0.8" + "google/gax": "^0.9" }, "extra": { "component": { diff --git a/tests/component/TestComposerInstall.php b/tests/component/TestComposerInstall.php new file mode 100644 index 000000000000..73fbfd08a4dc --- /dev/null +++ b/tests/component/TestComposerInstall.php @@ -0,0 +1,76 @@ +getComponents( + BASE_PATH .'/src', + BASE_PATH .'/composer.json' + ); + } +} + +$composer = []; +$composer['require'] = []; +$composer['repositories'] = []; +$composer['minimum-stability'] = 'dev'; + +foreach((new GetComponentsImpl)->components() as $component) { + if ($component['id'] === 'google-cloud') continue; + if ($component['id'] === 'cloud-core') continue; + + $composer['require']['google/'. $component['id']] = '*'; + $composer['repositories'][] = [ + 'type' => 'path', + 'url' => BASE_PATH .'/'. $component['path'] + ]; +} + +file_put_contents(__DIR__ .'/composer.json', json_encode($composer, JSON_UNESCAPED_SLASHES)); + +$out = []; +$return = null; +exec('composer install --dry-run --working-dir='. __DIR__ .' 2>&1', $out, $return); +unlink(__DIR__ .'/composer.json'); + +if ($return !== 0) { + echo "Problem installing components!" . PHP_EOL . PHP_EOL; + + echo implode("\n", $out); + exit(1); +} From 8412656fa05b041174b248b74064975494ca0a1b Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 25 May 2017 11:15:05 -0400 Subject: [PATCH 34/46] Prepare v0.32.0 (#516) * Prepare v0.32.0 * Bump monitoring, error reporting --- README.md | 16 ++++++++++++++++ docs/contents/google-cloud.json | 1 + docs/manifest.json | 14 ++++++++++++++ src/Core/VERSION | 2 +- src/ErrorReporting/VERSION | 2 +- src/Monitoring/VERSION | 2 +- src/ServiceBuilder.php | 2 +- src/Spanner/SpannerClient.php | 2 +- src/Spanner/VERSION | 2 +- src/Trace/VERSION | 2 +- 10 files changed, 38 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9e0b5f2e9627..5cd664e16bea 100644 --- a/README.md +++ b/README.md @@ -498,6 +498,14 @@ if ($operationResponse->operationSucceeded()) { } ``` +#### google/cloud-videointelligence + +Cloud Video Intelligence can be installed separately by requiring the `google/cloud-videointelligence` composer package: + +``` +$ require google/cloud-videointelligence +``` + ## Google Stackdriver Trace (Alpha) - [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/trace/traceclient) @@ -531,6 +539,14 @@ foreach($traceClient->traces() as $trace) { } ``` +#### google/cloud-trace + +Stackdriver Trace can be installed separately by requiring the `google/cloud-trace` composer package: + +``` +$ require google/cloud-trace +``` + ## Caching Access Tokens By default the library will use a simple in-memory caching implementation, however it is possible to override this behavior by passing a [PSR-6](http://www.php-fig.org/psr/psr-6/) caching implementation in to the desired client. diff --git a/docs/contents/google-cloud.json b/docs/contents/google-cloud.json index c7a5ba8e8932..224ba5a6d7fe 100644 --- a/docs/contents/google-cloud.json +++ b/docs/contents/google-cloud.json @@ -15,6 +15,7 @@ "cloud-spanner", "cloud-speech", "cloud-storage", + "cloud-trace", "cloud-translate", "cloud-videointelligence", "cloud-vision", diff --git a/docs/manifest.json b/docs/manifest.json index 92c98685ded1..e1edc53490c6 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -11,6 +11,7 @@ "name": "google/cloud", "defaultService": "servicebuilder", "versions": [ + "v0.32.0", "v0.31.1", "v0.31.0", "v0.30.1", @@ -67,6 +68,7 @@ "name": "google/cloud-core", "defaultService": "core/readme", "versions": [ + "v1.5.1", "v1.5.0", "v1.4.1", "v1.4.0", @@ -95,6 +97,7 @@ "name": "google/cloud-error-reporting", "defaultService": "errorreporting/readme", "versions": [ + "v0.3.0", "v0.2.1", "v0.2.0", "v0.1.0", @@ -118,6 +121,7 @@ "name": "google/cloud-monitoring", "defaultService": "monitoring/readme", "versions": [ + "v0.3.0", "v0.2.1", "v0.2.0", "v0.1.0", @@ -154,6 +158,7 @@ "name": "google/cloud-spanner", "defaultService": "spanner/spannerclient", "versions": [ + "v0.2.1", "v0.2.0", "v0.1.1", "v0.1.0", @@ -182,6 +187,15 @@ "master" ] }, + { + "id": "cloud-trace", + "name": "google/cloud-trace", + "defaultService": "trace/traceclient", + "versions": [ + "v0.1.0", + "master" + ] + }, { "id": "cloud-translate", "name": "google/cloud-translate", diff --git a/src/Core/VERSION b/src/Core/VERSION index 3e1ad720b13d..8e03717dca27 100644 --- a/src/Core/VERSION +++ b/src/Core/VERSION @@ -1 +1 @@ -1.5.0 \ No newline at end of file +1.5.1 \ No newline at end of file diff --git a/src/ErrorReporting/VERSION b/src/ErrorReporting/VERSION index 7dff5b892112..9325c3ccda98 100644 --- a/src/ErrorReporting/VERSION +++ b/src/ErrorReporting/VERSION @@ -1 +1 @@ -0.2.1 \ No newline at end of file +0.3.0 \ No newline at end of file diff --git a/src/Monitoring/VERSION b/src/Monitoring/VERSION index 7dff5b892112..9325c3ccda98 100644 --- a/src/Monitoring/VERSION +++ b/src/Monitoring/VERSION @@ -1 +1 @@ -0.2.1 \ No newline at end of file +0.3.0 \ No newline at end of file diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index f38af13b9299..590b28698efb 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -50,7 +50,7 @@ */ class ServiceBuilder { - const VERSION = '0.31.1'; + const VERSION = '0.32.0'; /** * @var array Configuration options to be used between clients. diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index a19803a68d5a..89a8491c0de7 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -65,7 +65,7 @@ class SpannerClient use LROTrait; use ValidateTrait; - const VERSION = '0.2.0'; + const VERSION = '0.2.1'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data'; const ADMIN_SCOPE = 'https://www.googleapis.com/auth/spanner.admin'; diff --git a/src/Spanner/VERSION b/src/Spanner/VERSION index 341cf11faf9a..7dff5b892112 100644 --- a/src/Spanner/VERSION +++ b/src/Spanner/VERSION @@ -1 +1 @@ -0.2.0 \ No newline at end of file +0.2.1 \ No newline at end of file diff --git a/src/Trace/VERSION b/src/Trace/VERSION index 6e8bf73aa550..6c6aa7cb0918 100644 --- a/src/Trace/VERSION +++ b/src/Trace/VERSION @@ -1 +1 @@ -0.1.0 +0.1.0 \ No newline at end of file From 63255af23bd84a15fe24b216553f678e5dac6684 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Thu, 25 May 2017 10:08:27 -0700 Subject: [PATCH 35/46] Fix TraceClient example on the main README.md (#517) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cd664e16bea..8e4e7ae7f1c6 100644 --- a/README.md +++ b/README.md @@ -518,7 +518,7 @@ require 'vendor/autoload.php'; use Google\Cloud\Trace\TraceClient; -$traceClient = new SpeechClient([ +$traceClient = new TraceClient([ 'projectId' => 'my_project' ]); From 1ad53b42b60d4b843e47929b86011bf055911fd4 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Fri, 26 May 2017 12:55:14 -0400 Subject: [PATCH 36/46] Correct the suggested commit for symfony/lock (#518) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 1945bfc2a393..fc697823f244 100644 --- a/composer.json +++ b/composer.json @@ -64,7 +64,7 @@ "suggest": { "google/gax": "Required to support gRPC", "google/proto-client-php": "Required to support gRPC", - "symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: dev-master#1ba6ac9" + "symfony/lock": "Required for the Spanner cached based session pool. Please require the following commit: 3.3.x-dev#1ba6ac9" }, "autoload": { "psr-4": { From cd436672a6acdcc1b9c6db7410ae3add32300d90 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Wed, 31 May 2017 08:17:04 -0400 Subject: [PATCH 37/46] Add Query system tests, fix value mapping bugs (#520) --- src/Spanner/Database.php | 29 ++ src/Spanner/ValueMapper.php | 8 +- tests/snippets/Spanner/DatabaseTest.php | 73 +++ tests/system/Spanner/QueryTest.php | 611 ++++++++++++++++++++++++ tests/system/Spanner/ReadTest.php | 156 ------ 5 files changed, 719 insertions(+), 158 deletions(-) create mode 100644 tests/system/Spanner/QueryTest.php diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 7205ca0bd6e0..0a55ff8b6f33 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -1086,6 +1086,35 @@ public function delete($table, KeySet $keySet, array $options = []) * $transaction = $result->transaction(); * ``` * + * ``` + * // Parameters which may be null must include an expected parameter type. + * $result = $database->execute('SELECT * FROM Posts WHERE lastModifiedTime = @timestamp', [ + * 'parameters' => [ + * 'timestamp' => $timestamp + * ], + * 'types' => [ + * 'timestamp' => ValueMapper::TYPE_TIMESTAMP + * ] + * ]); + * + * $neverEditedPosts = $result->rows(); + * ``` + * + * ``` + * // Array parameters which may be null or empty must include the array value type. + * $result = $database->execute('SELECT @emptyArrayOfIntegers as numbers', [ + * 'parameters' => [ + * 'emptyArrayOfIntegers' => [] + * ], + * 'types' => [ + * 'emptyArrayOfIntegers' => [ValueMapper::TYPE_ARRAY, ValueMapper::TYPE_INT64] + * ] + * ]); + * + * $row = $result->rows()->current(); + * $emptyArray = $row['numbers']; + * ``` + * * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ExecuteSqlRequest ExecuteSqlRequest * @codingStandardsIgnoreEnd diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index ca3d57c54998..99b5c0e94a20 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -242,7 +242,11 @@ private function decodeValue($value, array $type) break; case self::TYPE_STRUCT: - $value = $this->decodeValues($type['structType']['fields'], $value, Result::RETURN_ASSOCIATIVE); + $fields = isset($type['structType']['fields']) + ? $type['structType']['fields'] + : []; + + $value = $this->decodeValues($fields, $value, Result::RETURN_ASSOCIATIVE); break; case self::TYPE_FLOAT64: @@ -354,7 +358,7 @@ private function paramType($value, $givenType = null, $arrayType = null) $type = $this->typeObject( self::TYPE_ARRAY, - $this->typeObject((isset($types[0])) ? $types[0] : null), + $this->typeObject((isset($types[0])) ? $types[0] : $arrayType), 'arrayElementType' ); diff --git a/tests/snippets/Spanner/DatabaseTest.php b/tests/snippets/Spanner/DatabaseTest.php index 6c7488a619b4..14a58795a8d6 100644 --- a/tests/snippets/Spanner/DatabaseTest.php +++ b/tests/snippets/Spanner/DatabaseTest.php @@ -659,6 +659,79 @@ public function testExecuteBeginTransaction() $this->assertInstanceOf(Transaction::class, $res->returnVal()->transaction()); } + public function testExecuteWithParameterType() + { + $this->connection->executeStreamingSql(Argument::that(function ($arg) { + if (!isset($arg['params'])) return false; + if (!isset($arg['paramTypes'])) return false; + if ($arg['paramTypes']['timestamp']['code'] !== ValueMapper::TYPE_TIMESTAMP) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->resultGenerator([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'lastModifiedTime', + 'type' => [ + 'code' => ValueMapper::TYPE_TIMESTAMP + ] + ] + ] + ] + ], + 'values' => [null] + ])); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'execute', 3); + $snippet->addLocal('database', $this->database); + $snippet->addLocal('timestamp', null); + $snippet->addUse(ValueMapper::class); + + $res = $snippet->invoke('neverEditedPosts'); + $this->assertNull($res->returnVal()->current()['lastModifiedTime']); + } + + public function testExecuteWithEmptyArray() + { + $this->connection->executeStreamingSql(Argument::that(function ($arg) { + if (!isset($arg['params'])) return false; + if (!isset($arg['paramTypes'])) return false; + if ($arg['paramTypes']['emptyArrayOfIntegers']['code'] !== ValueMapper::TYPE_ARRAY) return false; + if ($arg['paramTypes']['emptyArrayOfIntegers']['arrayElementType']['code'] !== ValueMapper::TYPE_INT64) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->resultGenerator([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'numbers', + 'type' => [ + 'code' => ValueMapper::TYPE_ARRAY, + 'arrayElementType' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ] + ], + 'values' => [[]] + ])); + + $this->stubOperation(); + + $snippet = $this->snippetFromMethod(Database::class, 'execute', 4); + $snippet->addLocal('database', $this->database); + $snippet->addUse(ValueMapper::class); + + $res = $snippet->invoke('emptyArray'); + $this->assertEmpty($res->returnVal()); + } + public function testRead() { $this->connection->streamingRead(Argument::any()) diff --git a/tests/system/Spanner/QueryTest.php b/tests/system/Spanner/QueryTest.php new file mode 100644 index 000000000000..72a73fc57652 --- /dev/null +++ b/tests/system/Spanner/QueryTest.php @@ -0,0 +1,611 @@ +execute('SELECT 1'); + $row = $res->rows()->current(); + + $this->assertEquals(1, $row[0]); + } + + /** + * covers 20 + * @expectedException Google\Cloud\Core\Exception\BadRequestException + */ + public function testInvalidQueryFails() + { + $db = self::$database; + + $db->execute('badquery')->rows()->current(); + } + /** + * covers 21 + */ + public function testQueryReturnsArrayStruct() + { + $db = self::$database; + + $res = $db->execute('SELECT ARRAY(SELECT STRUCT(1, 2))'); + $row = $res->rows()->current(); + $this->assertEquals($row[0][0], [1,2]); + } + + /** + * covers 22 + */ + public function testQueryReturnsEmptyArrayStruct() + { + $db = self::$database; + + $res = $db->execute('SELECT ARRAY(SELECT STRUCT())'); + $row = $res->rows()->current(); + $this->assertEquals($row[0], [[]]); + } + + /** + * covers 23 + */ + public function testBindBoolParameter() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => true + ] + ]); + + $row = $res->rows()->current(); + $this->assertTrue($row['foo']); + } + + /** + * covers 24 + */ + public function testBindBoolParameterNull() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => ValueMapper::TYPE_BOOL + ] + ]); + + $row = $res->rows()->current(); + $this->assertNull($row['foo']); + } + + /** + * covers 25 + */ + public function testBindInt64Parameter() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => 1337 + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals(1337, $row['foo']); + } + + /** + * covers 25 + */ + public function testBindInt64ParameterWithInt64Class() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => new Int64('1337') + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals(1337, $row['foo']); + } + + /** + * covers 26 + */ + public function testBindNullIntParameter() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => ValueMapper::TYPE_INT64 + ] + ]); + + $row = $res->rows()->current(); + $this->assertNull($row['foo']); + } + + /** + * covers 27 + */ + public function testBindFloat64Parameter() + { + $db = self::$database; + + $pi = 3.1415; + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $pi + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals($pi, $row['foo']); + } + + /** + * covers 28 + */ + public function testBindFloat64ParameterNull() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => ValueMapper::TYPE_FLOAT64 + ] + ]); + + $row = $res->rows()->current(); + $this->assertNull($row['foo']); + } + + /** + * covers 29 + */ + public function testBindStringParameter() + { + $db = self::$database; + + $str = 'hello world'; + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $str + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals($str, $row['foo']); + } + + /** + * covers 30 + */ + public function testBindStringParameterNull() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => ValueMapper::TYPE_STRING + ] + ]); + + $row = $res->rows()->current(); + $this->assertNull($row['foo']); + } + + /** + * covers 31 + */ + public function testBindBytesParameter() + { + $db = self::$database; + + $str = 'hello world'; + $bytes = new Bytes($str); + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $bytes + ] + ]); + + $row = $res->rows()->current(); + $this->assertInstanceOf(Bytes::class, $row['foo']); + $this->assertEquals($str, base64_decode($bytes->formatAsString())); + $this->assertEquals($str, (string)$bytes->get()); + } + + /** + * covers 32 + */ + public function testBindBytesParameterNull() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => ValueMapper::TYPE_BYTES + ] + ]); + + $row = $res->rows()->current(); + $this->assertNull($row['foo']); + } + + /** + * covers 33 + */ + public function testBindTimestampParameter() + { + $db = self::$database; + + $ts = new Timestamp(new \DateTimeImmutable); + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $ts + ] + ]); + + $row = $res->rows()->current(); + $this->assertInstanceOf(Timestamp::class, $row['foo']); + $this->assertEquals($ts->get()->format('r'), $row['foo']->get()->format('r')); + } + + /** + * covers 34 + */ + public function testBindTimestampParameterNull() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => ValueMapper::TYPE_TIMESTAMP + ] + ]); + + $row = $res->rows()->current(); + $this->assertNull($row['foo']); + } + + /** + * covers 35 + */ + public function testBindDateParameter() + { + $db = self::$database; + + $ts = new Date(new \DateTimeImmutable); + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $ts + ] + ]); + + $row = $res->rows()->current(); + $this->assertInstanceOf(Date::class, $row['foo']); + $this->assertEquals($ts->get()->format('Y-m-d'), $row['foo']->get()->format('Y-m-d')); + } + + /** + * covers 36 + */ + public function testBindDateParameterNull() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => ValueMapper::TYPE_DATE + ] + ]); + + $row = $res->rows()->current(); + $this->assertNull($row['foo']); + } + + /** + * covers 37 + * covers 40 + * covers 43 + * covers 46 + * covers 49 + * covers 52 + * covers 55 + * @dataProvider arrayTypes + */ + public function testBindArrayOfType($value, $result = null, $resultType = null, callable $filter = null) + { + if (!$filter) { + $filter = function ($val) { return $val; }; + } + + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $value + ] + ]); + + $row = $res->rows()->current(); + $param = $filter($row['foo']); + + if ($resultType) { + $this->assertContainsOnlyInstancesOf($resultType, $row['foo']); + } + + $this->assertEquals($param, $result ?: $value); + } + + /** + * covers 41 + * covers 44 + * covers 47 + * covers 50 + * covers 53 + * covers 56 + * @dataProvider arrayTypesEmpty + */ + public function testBindEmptyArrayOfType($type) + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => [] + ], + 'types' => [ + 'param' => [ValueMapper::TYPE_ARRAY, $type] + ] + ]); + + $row = $res->rows()->current(); + + $this->assertEmpty($row['foo']); + } + + /** + * covers 39 + * covers 42 + * covers 45 + * covers 48 + * covers 51 + * covers 54 + * covers 56 + * @dataProvider arrayTypesNull + */ + public function testBindNullArrayOfType($type) + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => null + ], + 'types' => [ + 'param' => [ValueMapper::TYPE_ARRAY, $type] + ] + ]); + + $row = $res->rows()->current(); + + $this->assertNull($row['foo']); + } + + /** + * covers 58 + */ + public function testQueryInfinity() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => INF + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals(INF, $row['foo']); + } + + /** + * covers 59 + */ + public function testQueryNegativeInfinity() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => -INF + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals(-INF, $row['foo']); + } + + /** + * covers 60 + */ + public function testQueryNotANumber() + { + $db = self::$database; + + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => NAN + ] + ]); + + $row = $res->rows()->current(); + $this->assertTrue(is_nan($row['foo'])); + } + + /** + * covers 61 + */ + public function testQueryArrayOfSpecialFloatValues() + { + $db = self::$database; + + $vals = [INF, -INF, NAN]; + $res = $db->execute('SELECT @param as foo', [ + 'parameters' => [ + 'param' => $vals + ] + ]); + + $row = $res->rows()->current(); + $this->assertEquals($vals[0], $row['foo'][0]); + $this->assertEquals($vals[1], $row['foo'][1]); + $this->assertTrue(is_nan($row['foo'][2])); + } + + public function arrayTypes() + { + return [ + // boolean (covers 37) + [[true,true,false]], + + // int64 (covers 40) + [[5,4,3,2,1]], + + // float64 (covers 43) + [[3.14, 4.13, 1.43]], + + // string (covers 46) + [['hello','world','google','cloud']], + + // bytes (covers 49) + [ + [new Bytes('hello'), new Bytes('world'), new Bytes('google'), new Bytes('cloud')], + ['hello', 'world', 'google', 'cloud'], + Bytes::class, + function (array $res) { + foreach ($res as $idx => $val) { + $res[$idx] = (string) $val->get(); + } + + return $res; + } + ], + + // timestamp (covers 52) + [ + [new Timestamp(new \DateTime('2010-01-01')), new Timestamp(new \DateTime('2011-01-01')), new Timestamp(new \DateTime('2012-01-01'))], + ['2010-01-01', '2011-01-01', '2012-01-01'], + Timestamp::class, + function (array $res) { + foreach ($res as $idx => $val) { + $res[$idx] = $val->get()->format('Y-m-d'); + } + + return $res; + } + ], + + // date (covers 55) + [ + [new Date(new \DateTime('2010-01-01')), new Date(new \DateTime('2011-01-01')), new Date(new \DateTime('2012-01-01'))], + ['2010-01-01', '2011-01-01', '2012-01-01'], + Date::class, + function (array $res) { + foreach ($res as $idx => $val) { + $res[$idx] = $val->get()->format('Y-m-d'); + } + + return $res; + } + ] + ]; + } + + public function arrayTypesEmpty() + { + return [ + [ValueMapper::TYPE_BOOL], + [ValueMapper::TYPE_INT64], + [ValueMapper::TYPE_FLOAT64], + [ValueMapper::TYPE_STRING], + [ValueMapper::TYPE_BYTES], + [ValueMapper::TYPE_TIMESTAMP], + [ValueMapper::TYPE_DATE], + ]; + } + + public function arrayTypesNull() + { + return [ + [ValueMapper::TYPE_BOOL], + [ValueMapper::TYPE_INT64], + [ValueMapper::TYPE_FLOAT64], + [ValueMapper::TYPE_STRING], + [ValueMapper::TYPE_BYTES], + [ValueMapper::TYPE_TIMESTAMP], + [ValueMapper::TYPE_DATE], + ]; + } +} diff --git a/tests/system/Spanner/ReadTest.php b/tests/system/Spanner/ReadTest.php index 992489cf3bef..218f815500b5 100644 --- a/tests/system/Spanner/ReadTest.php +++ b/tests/system/Spanner/ReadTest.php @@ -208,163 +208,7 @@ public function testRangeReadPartialKeyClosed() $this->assertTrue(in_array(self::$dataset[10], $rows)); } - /** - * covers 21 - */ - public function testQueryReturnsArrayStruct() - { - $db = self::$database; - - $res = $db->execute('SELECT ARRAY(SELECT STRUCT(1, 2))'); - $row = $res->rows()->current(); - $this->assertEquals($row[0][0], [1,2]); - } - - /** - * covers 23 - */ - public function testBindBoolParameter() - { - $db = self::$database; - - $res = $db->execute('SELECT @param as foo', [ - 'parameters' => [ - 'param' => true - ] - ]); - - $row = $res->rows()->current(); - $this->assertTrue($row['foo']); - } - - /** - * covers 25 - */ - public function testBindInt64Parameter() - { - $db = self::$database; - $res = $db->execute('SELECT @param as foo', [ - 'parameters' => [ - 'param' => 1337 - ] - ]); - - $row = $res->rows()->current(); - $this->assertEquals(1337, $row['foo']); - } - - /** - * covers 25 - */ - public function testBindInt64ParameterWithInt64Class() - { - $db = self::$database; - - $res = $db->execute('SELECT @param as foo', [ - 'parameters' => [ - 'param' => new Int64('1337') - ] - ]); - - $row = $res->rows()->current(); - $this->assertEquals(1337, $row['foo']); - } - - /** - * covers 26 - */ - public function testBindNullIntParameter() - { - $db = self::$database; - - $res = $db->execute('SELECT @param as foo', [ - 'parameters' => [ - 'param' => null - ], - 'types' => [ - 'param' => ValueMapper::TYPE_INT64 - ] - ]); - - $row = $res->rows()->current(); - $this->assertNull($row['foo']); - } - - /** - * covers 27 - */ - public function testBindFloat64Parameter() - { - $db = self::$database; - - $pi = 3.1415; - $res = $db->execute('SELECT @param as foo', [ - 'parameters' => [ - 'param' => $pi - ] - ]); - - $row = $res->rows()->current(); - $this->assertEquals($pi, $row['foo']); - } - - /** - * covers 29 - */ - public function testBindStringParameter() - { - $db = self::$database; - - $str = 'hello world'; - $res = $db->execute('SELECT @param as foo', [ - 'parameters' => [ - 'param' => $str - ] - ]); - - $row = $res->rows()->current(); - $this->assertEquals($str, $row['foo']); - } - - /** - * covers 31 - */ - public function testBindBytesParameter() - { - $db = self::$database; - - $str = 'hello world'; - $bytes = new Bytes($str); - $res = $db->execute('SELECT @param as foo', [ - 'parameters' => [ - 'param' => $bytes - ] - ]); - - $row = $res->rows()->current(); - $this->assertInstanceOf(Bytes::class, $row['foo']); - $this->assertEquals($str, base64_decode($bytes->formatAsString())); - $this->assertEquals($str, (string)$bytes->get()); - } - - /** - * covers 40 - */ - public function testBindInt64ArrayParameter() - { - $db = self::$database; - - $arr = [5,4,3,2,1]; - $res = $db->execute('SELECT @param as foo', [ - 'parameters' => [ - 'param' => $arr - ] - ]); - - $row = $res->rows()->current(); - $this->assertEquals($arr, $row['foo']); - } private static function generateDataset($count = 20, $ordered = false) { From 846f2f97332df528d4a23e25fcc7af793651651c Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Wed, 31 May 2017 10:03:06 -0400 Subject: [PATCH 38/46] Update Translate service definition, add system tests, remove documentation for non-existent option (#522) --- src/Translate/Connection/Rest.php | 4 +- .../ServiceDefinition/translate-v2.json | 483 ++++++++++++------ src/Translate/TranslateClient.php | 16 +- tests/system/Translate/TranslateTest.php | 200 ++++++++ tests/system/Translate/TranslateTestCase.php | 39 ++ 5 files changed, 568 insertions(+), 174 deletions(-) create mode 100644 tests/system/Translate/TranslateTest.php create mode 100644 tests/system/Translate/TranslateTestCase.php diff --git a/src/Translate/Connection/Rest.php b/src/Translate/Connection/Rest.php index 9e67617246c2..ae7dd730a0ae 100644 --- a/src/Translate/Connection/Rest.php +++ b/src/Translate/Connection/Rest.php @@ -57,7 +57,7 @@ public function __construct(array $config = []) */ public function listDetections(array $args = []) { - return $this->send('detections', 'list', $args); + return $this->send('detections', 'detect', $args); } /** @@ -75,6 +75,6 @@ public function listLanguages(array $args = []) */ public function listTranslations(array $args = []) { - return $this->send('translations', 'list', $args); + return $this->send('translations', 'translate', $args); } } diff --git a/src/Translate/Connection/ServiceDefinition/translate-v2.json b/src/Translate/Connection/ServiceDefinition/translate-v2.json index b2db0ec998a1..092ed8ee2c2f 100644 --- a/src/Translate/Connection/ServiceDefinition/translate-v2.json +++ b/src/Translate/Connection/ServiceDefinition/translate-v2.json @@ -1,189 +1,237 @@ { - "kind": "discovery#restDescription", - "etag": "\"C5oy1hgQsABtYOYIOXWcR3BgYqU/6s__cFeA5l1i01rONlu3TmUQEHs\"", - "discoveryVersion": "v1", - "id": "translate:v2", - "name": "translate", - "version": "v2", - "revision": "20160627", - "title": "Translate API", - "description": "Translates text from one language to another.", - "ownerDomain": "google.com", - "ownerName": "Google", - "icons": { - "x16": "https://www.google.com/images/icons/product/translate-16.png", - "x32": "https://www.google.com/images/icons/product/translate-32.png" - }, - "documentationLink": "https://developers.google.com/translate/v2/using_rest", - "protocol": "rest", "baseUrl": "https://translation.googleapis.com/language/translate/", - "basePath": "/language/translate/", - "rootUrl": "https://translation.googleapis.com/", "servicePath": "language/translate/", - "batchPath": "batch", - "parameters": { - "alt": { - "type": "string", - "description": "Data format for the response.", - "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Responses with Content-Type of application/json" - ], - "location": "query" - }, - "fields": { - "type": "string", - "description": "Selector specifying which fields to include in a partial response.", - "location": "query" - }, - "key": { - "type": "string", - "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", - "location": "query" - }, - "oauth_token": { - "type": "string", - "description": "OAuth 2.0 token for the current user.", - "location": "query" - }, - "prettyPrint": { - "type": "boolean", - "description": "Returns response with indentations and line breaks.", - "default": "true", - "location": "query" - }, - "quotaUser": { - "type": "string", - "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", - "location": "query" - }, - "userIp": { - "type": "string", - "description": "IP address of the site where the request originates. Use this if you want to enforce per-user limits.", - "location": "query" - } - }, - "features": [ - "dataWrapper" - ], + "kind": "discovery#restDescription", + "description": "The Google Cloud Translation API lets websites and programs integrate with\n Google Translate programmatically.", + "basePath": "/language/translate/", + "id": "translate:v2", + "documentationLink": "https://code.google.com/apis/language/translate/v2/getting_started.html", + "revision": "20170525", + "discoveryVersion": "v1", "schemas": { - "DetectionsListResponse": { - "id": "DetectionsListResponse", + "TranslationsListResponse": { + "id": "TranslationsListResponse", + "description": "The main language translation response message.", "type": "object", "properties": { - "detections": { + "translations": { + "description": "Translations contains list of translation results of given text", "type": "array", - "description": "A detections contains detection results of several text", "items": { - "$ref": "DetectionsResource" + "$ref": "TranslationsResource" } } } }, - "DetectionsResource": { - "id": "DetectionsResource", - "type": "array", - "description": "An array of languages which we detect for the given text The most likely language list first.", - "items": { - "type": "object", - "properties": { - "confidence": { - "type": "number", - "description": "The confidence of the detection resul of this language.", - "format": "float" - }, - "isReliable": { - "type": "boolean", - "description": "A boolean to indicate is the language detection result reliable." - }, - "language": { - "type": "string", - "description": "The language we detect" + "TranslateTextRequest": { + "description": "The main translation request message for the Cloud Translation API.", + "type": "object", + "properties": { + "target": { + "type": "string", + "description": "The language to use for translation of the input text, set to one of the\nlanguage codes listed in Language Support." + }, + "q": { + "description": "The input text to translate. Repeat this parameter to perform translation\noperations on multiple text inputs.", + "type": "array", + "items": { + "type": "string" } + }, + "format": { + "type": "string", + "description": "The format of the source text, in either HTML (default) or plain-text. A\nvalue of \"html\" indicates HTML and a value of \"text\" indicates plain-text." + }, + "source": { + "description": "The language of the source text, set to one of the language codes listed in\nLanguage Support. If the source language is not specified, the API will\nattempt to identify the source language automatically and return it within\nthe response.", + "type": "string" + }, + "model": { + "description": "The `model` type requested for this translation. Valid values are\nlisted in public documentation.", + "type": "string" } - } + }, + "id": "TranslateTextRequest" }, - "LanguagesListResponse": { - "id": "LanguagesListResponse", + "DetectLanguageRequest": { + "id": "DetectLanguageRequest", + "description": "The request message for language detection.", "type": "object", "properties": { - "languages": { + "q": { + "description": "The input text upon which to perform language detection. Repeat this\nparameter to perform language detection on multiple text inputs.", "type": "array", - "description": "List of source/target languages supported by the translation API. If target parameter is unspecified, the list is sorted by the ASCII code point order of the language code. If target parameter is specified, the list is sorted by the collation order of the language name in the target language.", "items": { - "$ref": "LanguagesResource" + "type": "string" } } } }, "LanguagesResource": { - "id": "LanguagesResource", "type": "object", "properties": { "language": { - "type": "string", - "description": "The language code." + "description": "Supported language code, generally consisting of its ISO 639-1\nidentifier. (E.g. 'en', 'ja'). In certain cases, BCP-47 codes including\nlanguage + region identifiers are returned (e.g. 'zh-TW' and 'zh-CH')", + "type": "string" }, "name": { - "type": "string", - "description": "The localized name of the language if target parameter is given." + "description": "Human readable name of the language localized to the target language.", + "type": "string" } - } + }, + "id": "LanguagesResource" }, - "TranslationsListResponse": { - "id": "TranslationsListResponse", + "DetectionsListResponse": { "type": "object", "properties": { - "translations": { + "detections": { + "description": "A detections contains detection results of several text", "type": "array", - "description": "Translations contains list of translation results of given text", "items": { - "$ref": "TranslationsResource" + "$ref": "DetectionsResource" } } - } + }, + "id": "DetectionsListResponse" + }, + "GetSupportedLanguagesRequest": { + "description": "The request message for discovering supported languages.", + "type": "object", + "properties": { + "target": { + "description": "The language to use to return localized, human readable names of supported\nlanguages.", + "type": "string" + } + }, + "id": "GetSupportedLanguagesRequest" + }, + "LanguagesListResponse": { + "type": "object", + "properties": { + "languages": { + "type": "array", + "items": { + "$ref": "LanguagesResource" + }, + "description": "List of source/target languages supported by the translation API. If target parameter is unspecified, the list is sorted by the ASCII code point order of the language code. If target parameter is specified, the list is sorted by the collation order of the language name in the target language." + } + }, + "id": "LanguagesListResponse" + }, + "DetectionsResource": { + "description": "An array of languages which we detect for the given text The most likely language list first.", + "type": "array", + "items": { + "type": "object", + "properties": { + "confidence": { + "description": "The confidence of the detection result of this language.", + "format": "float", + "type": "number" + }, + "isReliable": { + "type": "boolean", + "description": "A boolean to indicate is the language detection result reliable." + }, + "language": { + "description": "The language we detected.", + "type": "string" + } + } + }, + "id": "DetectionsResource" }, "TranslationsResource": { - "id": "TranslationsResource", "type": "object", "properties": { - "detectedSourceLanguage": { - "type": "string", - "description": "Detected source language if source parameter is unspecified." + "model": { + "description": "The `model` type used for this translation. Valid values are\nlisted in public documentation. Can be different from requested `model`.\nPresent only if specific model type was explicitly requested.", + "type": "string" }, "translatedText": { - "type": "string", - "description": "The translation." + "description": "Text translated into the target language.", + "type": "string" + }, + "detectedSourceLanguage": { + "description": "The source language of the initial request, detected automatically, if\nno source language was passed within the initial request. If the\nsource language was passed, auto-detection of the language will not\noccur and this field will be empty.", + "type": "string" + } + }, + "id": "TranslationsResource" + } + }, + "icons": { + "x16": "https://www.google.com/images/icons/product/translate-16.png", + "x32": "https://www.google.com/images/icons/product/translate-32.png" + }, + "protocol": "rest", + "canonicalName": "Translate", + "auth": { + "oauth2": { + "scopes": { + "https://www.googleapis.com/auth/cloud-platform": { + "description": "View and manage your data across Google Cloud Platform services" + }, + "https://www.googleapis.com/auth/cloud-translation": { + "description": "Translate text from one language to another using Google Translate" } } } }, + "rootUrl": "https://translation.googleapis.com/", + "ownerDomain": "google.com", + "name": "translate", + "batchPath": "batch/translate", + "features": [ + "dataWrapper" + ], + "title": "Google Cloud Translation API", + "ownerName": "Google", "resources": { "detections": { "methods": { "list": { "id": "language.detections.list", "path": "v2/detect", + "description": "Detects the language of text within a request.", "httpMethod": "GET", - "description": "Detect the language of text.", + "response": { + "$ref": "DetectionsListResponse" + }, + "parameterOrder": [ + "q" + ], "parameters": { "q": { + "description": "The input text upon which to perform language detection. Repeat this\nparameter to perform language detection on multiple text inputs.", "type": "string", - "description": "The text to detect", "required": true, "repeated": true, "location": "query" } }, - "parameterOrder": [ - "q" - ], + "scopes": [ + "https://www.googleapis.com/auth/cloud-translation", + "https://www.googleapis.com/auth/cloud-platform" + ] + }, + "detect": { + "path": "v2/detect", + "id": "language.detections.detect", + "description": "Detects the language of text within a request.", + "request": { + "$ref": "DetectLanguageRequest" + }, "response": { "$ref": "DetectionsListResponse" - } + }, + "parameterOrder": [], + "httpMethod": "POST", + "parameters": {}, + "scopes": [ + "https://www.googleapis.com/auth/cloud-translation", + "https://www.googleapis.com/auth/cloud-platform" + ] } } }, @@ -192,82 +240,201 @@ "list": { "id": "language.languages.list", "path": "v2/languages", + "description": "Returns a list of supported languages for translation.", + "response": { + "$ref": "LanguagesListResponse" + }, "httpMethod": "GET", - "description": "List the source/target languages supported by the API", + "scopes": [ + "https://www.googleapis.com/auth/cloud-translation", + "https://www.googleapis.com/auth/cloud-platform" + ], "parameters": { "target": { - "type": "string", - "description": "the language and collation in which the localized results should be returned", - "location": "query" + "location": "query", + "description": "The language to use to return localized, human readable names of supported\nlanguages.", + "type": "string" + }, + "model": { + "location": "query", + "description": "The model type for which supported languages should be returned.", + "type": "string" } - }, - "response": { - "$ref": "LanguagesListResponse" } } } }, "translations": { "methods": { + "translate": { + "response": { + "$ref": "TranslationsListResponse" + }, + "parameterOrder": [], + "httpMethod": "POST", + "scopes": [ + "https://www.googleapis.com/auth/cloud-translation", + "https://www.googleapis.com/auth/cloud-platform" + ], + "parameters": {}, + "path": "v2", + "id": "language.translations.translate", + "request": { + "$ref": "TranslateTextRequest" + }, + "description": "Translates input text, returning translated text." + }, "list": { - "id": "language.translations.list", "path": "v2", + "id": "language.translations.list", + "description": "Translates input text, returning translated text.", + "response": { + "$ref": "TranslationsListResponse" + }, + "parameterOrder": [ + "q", + "target" + ], "httpMethod": "GET", - "description": "Returns text translations from one language to another.", "parameters": { - "cid": { - "type": "string", - "description": "The customization id for translate", - "repeated": true, - "location": "query" - }, - "model": { + "target": { + "location": "query", + "description": "The language to use for translation of the input text, set to one of the\nlanguage codes listed in Language Support.", "type": "string", - "description": "The model to use", - "repeated": false, - "location": "query" + "required": true }, "format": { - "type": "string", - "description": "The format of the text", + "location": "query", "enum": [ "html", "text" ], + "description": "The format of the source text, in either HTML (default) or plain-text. A\nvalue of \"html\" indicates HTML and a value of \"text\" indicates plain-text.", + "type": "string", "enumDescriptions": [ "Specifies the input is in HTML", "Specifies the input is in plain textual format" - ], - "location": "query" + ] + }, + "model": { + "location": "query", + "description": "The `model` type requested for this translation. Valid values are\nlisted in public documentation.", + "type": "string" }, "q": { + "description": "The input text to translate. Repeat this parameter to perform translation\noperations on multiple text inputs.", "type": "string", - "description": "The text to translate", "required": true, "repeated": true, "location": "query" }, "source": { + "description": "The language of the source text, set to one of the language codes listed in\nLanguage Support. If the source language is not specified, the API will\nattempt to identify the source language automatically and return it within\nthe response.", "type": "string", - "description": "The source language of the text", "location": "query" }, - "target": { + "cid": { + "description": "The customization id for translate", "type": "string", - "description": "The target language into which the text should be translated", - "required": true, + "repeated": true, "location": "query" } }, - "parameterOrder": [ - "q", - "target" - ], - "response": { - "$ref": "TranslationsListResponse" - } + "scopes": [ + "https://www.googleapis.com/auth/cloud-translation", + "https://www.googleapis.com/auth/cloud-platform" + ] } } } - } + }, + "parameters": { + "key": { + "description": "API key. Your API key identifies your project and provides you with API access, quota, and reports. Required unless you provide an OAuth 2.0 token.", + "type": "string", + "location": "query" + }, + "access_token": { + "location": "query", + "description": "OAuth access token.", + "type": "string" + }, + "quotaUser": { + "location": "query", + "description": "Available to use for quota purposes for server-side applications. Can be any arbitrary string assigned to a user, but should not exceed 40 characters. Overrides userIp if both are provided.", + "type": "string" + }, + "pp": { + "description": "Pretty-print response.", + "type": "boolean", + "default": "true", + "location": "query" + }, + "oauth_token": { + "location": "query", + "description": "OAuth 2.0 token for the current user.", + "type": "string" + }, + "bearer_token": { + "description": "OAuth bearer token.", + "type": "string", + "location": "query" + }, + "upload_protocol": { + "description": "Upload protocol for media (e.g. \"raw\", \"multipart\").", + "type": "string", + "location": "query" + }, + "prettyPrint": { + "location": "query", + "description": "Returns response with indentations and line breaks.", + "type": "boolean", + "default": "true" + }, + "fields": { + "type": "string", + "location": "query", + "description": "Selector specifying which fields to include in a partial response." + }, + "uploadType": { + "location": "query", + "description": "Legacy upload protocol for media (e.g. \"media\", \"multipart\").", + "type": "string" + }, + "callback": { + "type": "string", + "location": "query", + "description": "JSONP" + }, + "$.xgafv": { + "type": "string", + "enumDescriptions": [ + "v1 error format", + "v2 error format" + ], + "location": "query", + "enum": [ + "1", + "2" + ], + "description": "V1 error format." + }, + "alt": { + "enum": [ + "json", + "media", + "proto" + ], + "type": "string", + "enumDescriptions": [ + "Responses with Content-Type of application/json", + "Media download with context-dependent Content-Type", + "Responses with Content-Type of application/x-protobuf" + ], + "location": "query", + "description": "Data format for response.", + "default": "json" + } + }, + "version": "v2" } diff --git a/src/Translate/TranslateClient.php b/src/Translate/TranslateClient.php index 6c69dbdce179..f9ab4431baa7 100644 --- a/src/Translate/TranslateClient.php +++ b/src/Translate/TranslateClient.php @@ -260,13 +260,7 @@ public function translateBatch(array $strings, array $options = []) * @see https://cloud.google.com/translation/v2/detecting-language-with-rest Detecting Langauge * * @param string $string The string to detect the language of. - * @param array $options [optional] { - * Configuration Options. - * - * @type string $format Indicates whether the string is either - * plain-text or HTML. Acceptable values are `html` or `text`. - * **Defaults to** `"html"`. - * } + * @param array $options [optional] Configuration Options. * @return array A result including a `languageCode` key * containing the detected ISO 639-1 language code, an `input` key * containing the original string, and in most cases a `confidence` @@ -296,13 +290,7 @@ public function detectLanguage($string, array $options = []) * @see https://cloud.google.com/translation/v2/detecting-language-with-rest Detecting Langauge * * @param string $string The string to detect the language of. - * @param array $options [optional] { - * Configuration Options. - * - * @type string $format Indicates whether the string is either - * plain-text or HTML. Acceptable values are `html` or `text`. - * **Defaults to** `"html"`. - * } + * @param array $options [optional] Configuration Options. * @return array A set of results. Each result includes a `languageCode` key * containing the detected ISO 639-1 language code, an `input` key * containing the original string, and in most cases a `confidence` diff --git a/tests/system/Translate/TranslateTest.php b/tests/system/Translate/TranslateTest.php new file mode 100644 index 000000000000..03e8ddea68fd --- /dev/null +++ b/tests/system/Translate/TranslateTest.php @@ -0,0 +1,200 @@ +translate(self::INPUT_STRING); + $this->assertEquals(self::INPUT_LANGUAGE, $res['source']); + $this->assertEquals(self::INPUT_STRING, $res['input']); + $this->assertEquals(self::OUTPUT_STRING, $res['text']); + $this->assertNull($res['model']); + } + + public function testTranslateModelNmt() + { + $client = self::$client; + + $res = $client->translate(self::INPUT_STRING, ['model' => 'nmt']); + $this->assertEquals(self::INPUT_LANGUAGE, $res['source']); + $this->assertEquals(self::INPUT_STRING, $res['input']); + $this->assertEquals(self::OUTPUT_STRING, $res['text']); + $this->assertEquals('nmt', $res['model']); + } + + public function testTranslateModelBase() + { + $client = self::$client; + + $res = $client->translate(self::INPUT_STRING, ['model' => 'base']); + $this->assertEquals(self::INPUT_LANGUAGE, $res['source']); + $this->assertEquals(self::INPUT_STRING, $res['input']); + $this->assertEquals(self::OUTPUT_STRING, $res['text']); + $this->assertEquals('base', $res['model']); + } + + /** + * @expectedException Google\Cloud\Core\Exception\BadRequestException + */ + public function testTranslateInvalidModel() + { + self::$client->translate(self::INPUT_STRING, ['model' => 'thisDoesntActuallyExistSoGimmeErrorPlease']); + } + + /** + * @expectedException Google\Cloud\Core\Exception\BadRequestException + */ + public function testTranslateInvalidTarget() + { + self::$client->translate(self::INPUT_STRING, ['target' => 'thisDoesntActuallyExistSoGimmeErrorPlease']); + } + + /** + * @expectedException Google\Cloud\Core\Exception\BadRequestException + */ + public function testTranslateInvalidSource() + { + self::$client->translate(self::INPUT_STRING, ['source' => 'thisDoesntActuallyExistSoGimmeErrorPlease']); + } + + public function testTranslateBatch() + { + $client = self::$client; + + $res = $client->translateBatch([self::INPUT_STRING]); + $this->assertEquals(self::INPUT_LANGUAGE, $res[0]['source']); + $this->assertEquals(self::INPUT_STRING, $res[0]['input']); + $this->assertEquals(self::OUTPUT_STRING, $res[0]['text']); + $this->assertNull($res[0]['model']); + } + + public function testTranslateBatchModelNmt() + { + $client = self::$client; + + $res = $client->translateBatch([self::INPUT_STRING], ['model' => 'nmt']); + $this->assertEquals(self::INPUT_LANGUAGE, $res[0]['source']); + $this->assertEquals(self::INPUT_STRING, $res[0]['input']); + $this->assertEquals(self::OUTPUT_STRING, $res[0]['text']); + $this->assertEquals('nmt', $res[0]['model']); + } + + public function testTranslateBatchModelBase() + { + $client = self::$client; + + $res = $client->translateBatch([self::INPUT_STRING], ['model' => 'base']); + $this->assertEquals(self::INPUT_LANGUAGE, $res[0]['source']); + $this->assertEquals(self::INPUT_STRING, $res[0]['input']); + $this->assertEquals(self::OUTPUT_STRING, $res[0]['text']); + $this->assertEquals('base', $res[0]['model']); + } + + /** + * @expectedException Google\Cloud\Core\Exception\BadRequestException + */ + public function testTranslateBatchInvalidModel() + { + self::$client->translateBatch([self::INPUT_STRING], ['model' => 'thisDoesntActuallyExistSoGimmeErrorPlease']); + } + + /** + * @expectedException Google\Cloud\Core\Exception\BadRequestException + */ + public function testTranslateBatchInvalidTarget() + { + self::$client->translateBatch([self::INPUT_STRING], ['target' => 'thisDoesntActuallyExistSoGimmeErrorPlease']); + } + + /** + * @expectedException Google\Cloud\Core\Exception\BadRequestException + */ + public function testTranslateBatchInvalidSource() + { + self::$client->translateBatch([self::INPUT_STRING], ['source' => 'thisDoesntActuallyExistSoGimmeErrorPlease']); + } + + public function testDetectLanguage() + { + $client = self::$client; + + $res = $client->detectLanguage(self::INPUT_STRING); + $this->assertEquals(self::INPUT_LANGUAGE, $res['languageCode']); + $this->assertEquals(self::INPUT_STRING, $res['input']); + $this->assertTrue(is_double($res['confidence'])); + } + + public function testDetectLanguageUndefined() + { + $client = self::$client; + + $res = $client->detectLanguage(''); + $this->assertEquals('und', $res['languageCode']); + $this->assertEquals(1, $res['confidence']); + } + + public function testDetectLanguageBatch() + { + $client = self::$client; + + $res = $client->detectLanguageBatch([self::INPUT_STRING]); + $this->assertEquals(self::INPUT_LANGUAGE, $res[0]['languageCode']); + $this->assertEquals(self::INPUT_STRING, $res[0]['input']); + $this->assertTrue(is_double($res[0]['confidence'])); + } + + public function testDetectLanguageBatchUndefined() + { + $client = self::$client; + + $res = $client->detectLanguageBatch(['']); + $this->assertEquals('und', $res[0]['languageCode']); + $this->assertEquals(1, $res[0]['confidence']); + } + + public function testDetectLanguages() + { + $client = self::$client; + + $res = $client->languages(); + $this->assertTrue(is_array($res)); + $this->assertTrue(in_array('en', $res)); + $this->assertTrue(in_array('es', $res)); + } + + public function testLocalizedLanguages() + { + $client = self::$client; + + $res = $client->localizedLanguages(); + $this->assertTrue(is_array($res)); + $this->assertTrue(in_array(['code' => 'es', 'name' => 'Spanish'], $res)); + $this->assertTrue(in_array(['code' => 'en', 'name' => 'English'], $res)); + } +} diff --git a/tests/system/Translate/TranslateTestCase.php b/tests/system/Translate/TranslateTestCase.php new file mode 100644 index 000000000000..d1fa38278137 --- /dev/null +++ b/tests/system/Translate/TranslateTestCase.php @@ -0,0 +1,39 @@ + $keyFilePath + ]); + } +} From 3c917dcf0864a98036b2a24267492f195545fe9a Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 1 Jun 2017 12:02:04 -0400 Subject: [PATCH 39/46] Prepare v0.32.1 (#523) --- docs/manifest.json | 3 +++ src/ServiceBuilder.php | 2 +- src/Spanner/SpannerClient.php | 2 +- src/Spanner/VERSION | 2 +- src/Translate/TranslateClient.php | 2 +- src/Translate/VERSION | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/manifest.json b/docs/manifest.json index e1edc53490c6..4f6e13dda9a6 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -11,6 +11,7 @@ "name": "google/cloud", "defaultService": "servicebuilder", "versions": [ + "v0.32.1", "v0.32.0", "v0.31.1", "v0.31.0", @@ -158,6 +159,7 @@ "name": "google/cloud-spanner", "defaultService": "spanner/spannerclient", "versions": [ + "v0.2.2", "v0.2.1", "v0.2.0", "v0.1.1", @@ -201,6 +203,7 @@ "name": "google/cloud-translate", "defaultService": "translate/translateclient", "versions": [ + "v0.2.1", "v0.2.0", "v0.1.0", "master" diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index 590b28698efb..b4798b35b958 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -50,7 +50,7 @@ */ class ServiceBuilder { - const VERSION = '0.32.0'; + const VERSION = '0.32.1'; /** * @var array Configuration options to be used between clients. diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 89a8491c0de7..92d60e364b11 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -65,7 +65,7 @@ class SpannerClient use LROTrait; use ValidateTrait; - const VERSION = '0.2.1'; + const VERSION = '0.2.2'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data'; const ADMIN_SCOPE = 'https://www.googleapis.com/auth/spanner.admin'; diff --git a/src/Spanner/VERSION b/src/Spanner/VERSION index 7dff5b892112..f4778493c500 100644 --- a/src/Spanner/VERSION +++ b/src/Spanner/VERSION @@ -1 +1 @@ -0.2.1 \ No newline at end of file +0.2.2 \ No newline at end of file diff --git a/src/Translate/TranslateClient.php b/src/Translate/TranslateClient.php index f9ab4431baa7..196e63b7b483 100644 --- a/src/Translate/TranslateClient.php +++ b/src/Translate/TranslateClient.php @@ -51,7 +51,7 @@ class TranslateClient { use ClientTrait; - const VERSION = '0.2.0'; + const VERSION = '0.2.1'; const ENGLISH_LANGUAGE_CODE = 'en'; diff --git a/src/Translate/VERSION b/src/Translate/VERSION index 341cf11faf9a..7dff5b892112 100644 --- a/src/Translate/VERSION +++ b/src/Translate/VERSION @@ -1 +1 @@ -0.2.0 \ No newline at end of file +0.2.1 \ No newline at end of file From bd4bcc8ebc3f10fc1b8d5d120c519a62b7ca9961 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 8 Jun 2017 08:51:49 -0400 Subject: [PATCH 40/46] Add support for bucket labels (#529) --- src/Storage/Bucket.php | 4 +++ .../ServiceDefinition/storage-v1.json | 8 +++++ tests/system/Storage/ManageBucketsTest.php | 36 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/src/Storage/Bucket.php b/src/Storage/Bucket.php index ae3eedaccf4b..08a2a1c78675 100644 --- a/src/Storage/Bucket.php +++ b/src/Storage/Bucket.php @@ -532,6 +532,7 @@ public function delete(array $options = []) * ``` * * @see https://cloud.google.com/storage/docs/json_api/v1/buckets/patch Buckets patch API documentation. + * @see https://cloud.google.com/storage/docs/key-terms#bucket-labels Bucket Labels * * @param array $options [optional] { * Configuration options. @@ -571,6 +572,9 @@ public function delete(array $options = []) * `"STANDARD"` and `"DURABLE_REDUCED_AVAILABILITY"`. * @type array $versioning The bucket's versioning configuration. * @type array $website The bucket's website configuration. + * @type array $labels The Bucket labels. Labels are represented as an + * array of keys and values. To remove an existing label, set its + * value to `null`. * } * @return array */ diff --git a/src/Storage/Connection/ServiceDefinition/storage-v1.json b/src/Storage/Connection/ServiceDefinition/storage-v1.json index 9a8597124c0b..7e8342a53e43 100644 --- a/src/Storage/Connection/ServiceDefinition/storage-v1.json +++ b/src/Storage/Connection/ServiceDefinition/storage-v1.json @@ -163,6 +163,14 @@ "description": "The kind of item this is. For buckets, this is always storage#bucket.", "default": "storage#bucket" }, + "labels": { + "type": "object", + "description": "User-provided labels, in key/value pairs.", + "additionalProperties": { + "type": "string", + "description": "An individual label entry." + } + }, "lifecycle": { "type": "object", "description": "The bucket's lifecycle configuration. See lifecycle management for more information.", diff --git a/tests/system/Storage/ManageBucketsTest.php b/tests/system/Storage/ManageBucketsTest.php index 001539118604..7079fe7633f3 100644 --- a/tests/system/Storage/ManageBucketsTest.php +++ b/tests/system/Storage/ManageBucketsTest.php @@ -19,6 +19,7 @@ /** * @group storage + * @group storage-bucket */ class ManageBucketsTest extends StorageTestCase { @@ -122,4 +123,39 @@ public function testIam() $test = $iam->testPermissions($permissions); $this->assertEquals($permissions, $test); } + + public function testLabels() + { + $bucket = self::$bucket; + + $bucket->update([ + 'labels' => [ + 'foo' => 'bar' + ] + ]); + + $bucket->reload(); + + $this->assertEquals($bucket->info()['labels']['foo'], 'bar'); + + $bucket->update([ + 'labels' => [ + 'foo' => 'bat' + ] + ]); + + $bucket->reload(); + + $this->assertEquals($bucket->info()['labels']['foo'], 'bat'); + + $bucket->update([ + 'labels' => [ + 'foo' => null + ] + ]); + + $bucket->reload(); + + $this->assertFalse(isset($bucket->info()['labels']['foo'])); + } } From c43bbad682ec82028c6629937d9ccfe01eb11643 Mon Sep 17 00:00:00 2001 From: pvm1987 Date: Thu, 8 Jun 2017 15:52:10 +0300 Subject: [PATCH 41/46] Added new option "uploadProgressCallback" to method $bucket->upload(). (#412) * Added new option "uploadProgressCallback" to method $bucket->upload(). It's a callable parameter which defines a callback function/method to be triggered on each successfully uploaded chunk if resumable upload is used. * Include forgotten namespace. * Added description of the new constructor method in class ResumableUploader. * Added missing namespace dependency. * Alphabetical order in used namespaces. Fixed option name in constructor's description. Syntax changes to follow the project's consistency. Used cached getSize() value instead of additional calculations on each upload iteration. * Added test method for showing how to use the 'uploadProgressCallback' option for tracking the upload progress. * Fix some Travis build requirements to pass the build successfully. * Fixed some Travis build requirements to pass the build successfully. * Rollback .gitignore changes --- src/Core/Upload/ResumableUploader.php | 54 +++++++++++++++++++++- src/Storage/Bucket.php | 9 ++++ src/Storage/Connection/Rest.php | 3 +- tests/system/Storage/UploadObjectsTest.php | 33 +++++++++++++ 4 files changed, 96 insertions(+), 3 deletions(-) diff --git a/src/Core/Upload/ResumableUploader.php b/src/Core/Upload/ResumableUploader.php index 3d099cff04c6..dd45c61af655 100644 --- a/src/Core/Upload/ResumableUploader.php +++ b/src/Core/Upload/ResumableUploader.php @@ -19,10 +19,12 @@ use Google\Cloud\Core\Exception\GoogleException; use Google\Cloud\Core\JsonTrait; +use Google\Cloud\Core\RequestWrapper; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\LimitStream; use GuzzleHttp\Psr7\Request; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; /** * Resumable upload implementation. @@ -30,6 +32,11 @@ class ResumableUploader extends AbstractUploader { use JsonTrait; + + /** + * @var callable + */ + private $uploadProgressCallback; /** * @var int @@ -40,6 +47,41 @@ class ResumableUploader extends AbstractUploader * @var string */ private $resumeUri; + + /** + * Extend the parent constructor with the specific + * for resumable upload option "uploadProgressCallback" + * + * @param RequestWrapper $requestWrapper + * @param string|resource|StreamInterface $data + * @param string $uri + * @param array $options [optional] { + * Optional configuration. + * + * @type array $metadata Metadata on the resource. + * @type callable $uploadProgressCallback to be called on each + * successfully uploaded chunk. + * @type int $chunkSize Size of the chunks to send incrementally during + * a resumable upload. Must be in multiples of 262144 bytes. + * @type array $restOptions HTTP client specific configuration options. + * @type int $retries Number of retries for a failed request. + * **Defaults to** `3`. + * @type string $contentType Content type of the resource. + * } + */ + public function __construct( + RequestWrapper $requestWrapper, + $data, + $uri, + array $options = [] + ) { + parent::__construct($requestWrapper, $data, $uri, $options); + + // Set uploadProgressCallback if it's passed as an option. + if (isset($options['uploadProgressCallback'])) { + $this->uploadProgressCallback = $options['uploadProgressCallback']; + } + } /** * Gets the resume URI. @@ -99,9 +141,13 @@ public function upload() $this->chunkSize ?: - 1, $rangeStart ); - $rangeEnd = $rangeStart + ($data->getSize() - 1); + + $currStreamLimitSize = $data->getSize(); + + $rangeEnd = $rangeStart + ($currStreamLimitSize - 1); + $headers = [ - 'Content-Length' => $data->getSize(), + 'Content-Length' => $currStreamLimitSize, 'Content-Type' => $this->contentType, 'Content-Range' => "bytes $rangeStart-$rangeEnd/$size", ]; @@ -121,6 +167,10 @@ public function upload() $ex->getCode() ); } + + if (is_callable($this->uploadProgressCallback)) { + call_user_func($this->uploadProgressCallback, $currStreamLimitSize); + } $rangeStart = $this->getRangeStart($response->getHeaderLine('Range')); } while ($response->getStatusCode() === 308); diff --git a/src/Storage/Bucket.php b/src/Storage/Bucket.php index 08a2a1c78675..a4e81a97cf60 100644 --- a/src/Storage/Bucket.php +++ b/src/Storage/Bucket.php @@ -208,6 +208,15 @@ public function exists() * The size must be in multiples of 262144 bytes. With chunking * you have increased reliability at the risk of higher overhead. * It is recommended to not use chunking. + * @type callable $uploadProgressCallback If provided together with + * $resumable == true the given callable function/method will be + * called after each successfully uploaded chunk. The callable + * function/method will receive the number of uploaded bytes + * after each uploaded chunk as a parameter to this callable. + * It's useful if you want to create a progress bar when using + * resumable upload type together with $chunkSize parameter. + * If $chunkSize is not set the callable function/method will be + * called only once after the successful file upload. * @type string $predefinedAcl Predefined ACL to apply to the object. * Acceptable values include, `"authenticatedRead"`, * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, diff --git a/src/Storage/Connection/Rest.php b/src/Storage/Connection/Rest.php index 7dae6370daa9..d5da3f53afa2 100644 --- a/src/Storage/Connection/Rest.php +++ b/src/Storage/Connection/Rest.php @@ -302,7 +302,8 @@ private function resolveUploadOptions(array $args) 'requestTimeout', 'chunkSize', 'contentType', - 'metadata' + 'metadata', + 'uploadProgressCallback' ]; $args['uploaderOptions'] = array_intersect_key($args, array_flip($uploaderOptionKeys)); diff --git a/tests/system/Storage/UploadObjectsTest.php b/tests/system/Storage/UploadObjectsTest.php index 17d22dd34df0..6bb9963fe9eb 100644 --- a/tests/system/Storage/UploadObjectsTest.php +++ b/tests/system/Storage/UploadObjectsTest.php @@ -94,4 +94,37 @@ public function testUploadsObjectWithCustomerSuppliedEncryption() $this->assertEquals($sha, $object->info()['customerEncryption']['keySha256']); $this->assertEquals(strlen($data), $object->info()['size']); } + + private $testFileSize = 0; + private $totalStoredBytes = 0; + + public function testUploadsObjectWithProgressTracking() + { + $path = __DIR__ . '/../data/5mb.txt'; + + $this->testFileSize = filesize($path); + + $options = [ + 'resumable' => true, // It's required to be in resumable upload if we want to track the progress with callback method. + 'chunkSize' => 1 * 1024 * 1024, //1MB; The upload progress will be done in chunks. The size must be in multiples of 262144 bytes. + 'uploadProgressCallback' => array($this, 'onStoredFileChunk') + ]; + + $object = self::$bucket->upload(fopen($path, 'r'), $options); + + self::$deletionQueue[] = $object; + + $this->assertEquals('5mb.txt', $object->name()); + } + + public function onStoredFileChunk($storedBytes) + { + $this->totalStoredBytes += $storedBytes; + + $this->assertFalse($this->testFileSize < $this->totalStoredBytes); + + if ($this->testFileSize == $this->totalStoredBytes) { + $this->assertEquals(filesize(__DIR__ . '/../data/5mb.txt'), $this->totalStoredBytes); + } + } } From d75e73358176037dfd40496918b95a07ccd47be1 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Wed, 14 Jun 2017 16:00:34 -0400 Subject: [PATCH 42/46] Move type constants to Database (#532) --- src/Spanner/Bytes.php | 2 +- src/Spanner/Database.php | 29 ++++++---- src/Spanner/Date.php | 2 +- src/Spanner/Timestamp.php | 2 +- src/Spanner/Transaction.php | 14 ++--- src/Spanner/TransactionalReadTrait.php | 14 ++--- tests/snippets/Spanner/BytesTest.php | 3 +- tests/snippets/Spanner/DatabaseTest.php | 30 +++++------ tests/snippets/Spanner/DateTest.php | 3 +- tests/snippets/Spanner/SnapshotTest.php | 4 +- tests/snippets/Spanner/TimestampTest.php | 3 +- tests/snippets/Spanner/TransactionTest.php | 2 +- tests/system/Spanner/QueryTest.php | 48 ++++++++--------- tests/unit/Spanner/DatabaseTest.php | 2 +- tests/unit/Spanner/OperationTest.php | 5 +- tests/unit/Spanner/TransactionTest.php | 3 +- tests/unit/Spanner/ValueMapperTest.php | 61 +++++++++++----------- 17 files changed, 122 insertions(+), 105 deletions(-) diff --git a/src/Spanner/Bytes.php b/src/Spanner/Bytes.php index d3a17aa42094..8bb4447edf0b 100644 --- a/src/Spanner/Bytes.php +++ b/src/Spanner/Bytes.php @@ -80,7 +80,7 @@ public function get() */ public function type() { - return ValueMapper::TYPE_BYTES; + return Database::TYPE_BYTES; } /** diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 0a55ff8b6f33..435a82552ae2 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -33,6 +33,7 @@ use Google\Cloud\Spanner\Transaction; use Google\Cloud\Spanner\V1\SpannerClient as GrpcSpannerClient; use Google\GAX\ValidationException; +use google\spanner\v1\TypeCode; /** * Represents a Cloud Spanner Database. @@ -98,6 +99,16 @@ class Database const MAX_RETRIES = 10; + const TYPE_BOOL = TypeCode::TYPE_BOOL; + const TYPE_INT64 = TypeCode::TYPE_INT64; + const TYPE_FLOAT64 = TypeCode::TYPE_FLOAT64; + const TYPE_TIMESTAMP = TypeCode::TYPE_TIMESTAMP; + const TYPE_DATE = TypeCode::TYPE_DATE; + const TYPE_STRING = TypeCode::TYPE_STRING; + const TYPE_BYTES = TypeCode::TYPE_BYTES; + const TYPE_ARRAY = TypeCode::TYPE_ARRAY; + const TYPE_STRUCT = TypeCode::TYPE_STRUCT; + /** * @var ConnectionInterface */ @@ -1093,7 +1104,7 @@ public function delete($table, KeySet $keySet, array $options = []) * 'timestamp' => $timestamp * ], * 'types' => [ - * 'timestamp' => ValueMapper::TYPE_TIMESTAMP + * 'timestamp' => Database::TYPE_TIMESTAMP * ] * ]); * @@ -1107,7 +1118,7 @@ public function delete($table, KeySet $keySet, array $options = []) * 'emptyArrayOfIntegers' => [] * ], * 'types' => [ - * 'emptyArrayOfIntegers' => [ValueMapper::TYPE_ARRAY, ValueMapper::TYPE_INT64] + * 'emptyArrayOfIntegers' => [Database::TYPE_ARRAY, Database::TYPE_INT64] * ] * ]); * @@ -1137,14 +1148,14 @@ public function delete($table, KeySet $keySet, array $options = []) * definitions are only necessary for null parameter values. * Accepted values are defined as constants on * {@see Google\Cloud\Spanner\ValueMapper}, and are as follows: - * `ValueMapper::TYPE_BOOL`, `ValueMapper::TYPE_INT64`, - * `ValueMapper::TYPE_FLOAT64`, `ValueMapper::TYPE_TIMESTAMP`, - * `ValueMapper::TYPE_DATE`, `ValueMapper::TYPE_STRING`, - * `ValueMapper::TYPE_BYTES`, `ValueMapper::TYPE_ARRAY` and - * `ValueMapper::TYPE_STRUCT`. If the parameter type is an array, + * `Database::TYPE_BOOL`, `Database::TYPE_INT64`, + * `Database::TYPE_FLOAT64`, `Database::TYPE_TIMESTAMP`, + * `Database::TYPE_DATE`, `Database::TYPE_STRING`, + * `Database::TYPE_BYTES`, `Database::TYPE_ARRAY` and + * `Database::TYPE_STRUCT`. If the parameter type is an array, * the type should be given as an array, where the first element - * is `ValueMapper::TYPE_ARRAY` and the second element is the - * array type, for instance `[ValueMapper::TYPE_ARRAY, ValueMapper::TYPE_INT64]`. + * is `Database::TYPE_ARRAY` and the second element is the + * array type, for instance `[Database::TYPE_ARRAY, Database::TYPE_INT64]`. * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected * read timestamp is included in the Transaction message that * describes the transaction. diff --git a/src/Spanner/Date.php b/src/Spanner/Date.php index 713ae879e06d..25798e3d1d80 100644 --- a/src/Spanner/Date.php +++ b/src/Spanner/Date.php @@ -100,7 +100,7 @@ public function get() */ public function type() { - return ValueMapper::TYPE_DATE; + return Database::TYPE_DATE; } /** diff --git a/src/Spanner/Timestamp.php b/src/Spanner/Timestamp.php index 61ee9014c572..9ee243fc8ab9 100644 --- a/src/Spanner/Timestamp.php +++ b/src/Spanner/Timestamp.php @@ -94,7 +94,7 @@ public function get() */ public function type() { - return ValueMapper::TYPE_TIMESTAMP; + return Database::TYPE_TIMESTAMP; } /** diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 7296c7efa35f..8fad7e41e57b 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -96,14 +96,14 @@ * definitions are only necessary for null parameter values. * Accepted values are defined as constants on * {@see Google\Cloud\Spanner\ValueMapper}, and are as follows: - * `ValueMapper::TYPE_BOOL`, `ValueMapper::TYPE_INT64`, - * `ValueMapper::TYPE_FLOAT64`, `ValueMapper::TYPE_TIMESTAMP`, - * `ValueMapper::TYPE_DATE`, `ValueMapper::TYPE_STRING`, - * `ValueMapper::TYPE_BYTES`, `ValueMapper::TYPE_ARRAY` and - * `ValueMapper::TYPE_STRUCT`. If the parameter type is an array, + * `Database::TYPE_BOOL`, `Database::TYPE_INT64`, + * `Database::TYPE_FLOAT64`, `Database::TYPE_TIMESTAMP`, + * `Database::TYPE_DATE`, `Database::TYPE_STRING`, + * `Database::TYPE_BYTES`, `Database::TYPE_ARRAY` and + * `Database::TYPE_STRUCT`. If the parameter type is an array, * the type should be given as an array, where the first element - * is `ValueMapper::TYPE_ARRAY` and the second element is the - * array type, for instance `[ValueMapper::TYPE_ARRAY, ValueMapper::TYPE_INT64]`. + * is `Database::TYPE_ARRAY` and the second element is the + * array type, for instance `[Database::TYPE_ARRAY, Database::TYPE_INT64]`. * } * @return Result * } diff --git a/src/Spanner/TransactionalReadTrait.php b/src/Spanner/TransactionalReadTrait.php index c220e3ed61ce..017c94c0ea41 100644 --- a/src/Spanner/TransactionalReadTrait.php +++ b/src/Spanner/TransactionalReadTrait.php @@ -76,14 +76,14 @@ trait TransactionalReadTrait * definitions are only necessary for null parameter values. * Accepted values are defined as constants on * {@see Google\Cloud\Spanner\ValueMapper}, and are as follows: - * `ValueMapper::TYPE_BOOL`, `ValueMapper::TYPE_INT64`, - * `ValueMapper::TYPE_FLOAT64`, `ValueMapper::TYPE_TIMESTAMP`, - * `ValueMapper::TYPE_DATE`, `ValueMapper::TYPE_STRING`, - * `ValueMapper::TYPE_BYTES`, `ValueMapper::TYPE_ARRAY` and - * `ValueMapper::TYPE_STRUCT`. If the parameter type is an array, + * `Database::TYPE_BOOL`, `Database::TYPE_INT64`, + * `Database::TYPE_FLOAT64`, `Database::TYPE_TIMESTAMP`, + * `Database::TYPE_DATE`, `Database::TYPE_STRING`, + * `Database::TYPE_BYTES`, `Database::TYPE_ARRAY` and + * `Database::TYPE_STRUCT`. If the parameter type is an array, * the type should be given as an array, where the first element - * is `ValueMapper::TYPE_ARRAY` and the second element is the - * array type, for instance `[ValueMapper::TYPE_ARRAY, ValueMapper::TYPE_INT64]`. + * is `Database::TYPE_ARRAY` and the second element is the + * array type, for instance `[Database::TYPE_ARRAY, Database::TYPE_INT64]`. * } * @return Result */ diff --git a/tests/snippets/Spanner/BytesTest.php b/tests/snippets/Spanner/BytesTest.php index 00209c6f3e8c..82ca033adec6 100644 --- a/tests/snippets/Spanner/BytesTest.php +++ b/tests/snippets/Spanner/BytesTest.php @@ -19,6 +19,7 @@ use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Spanner\Bytes; +use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\ValueMapper; use Psr\Http\Message\StreamInterface; @@ -71,7 +72,7 @@ public function testType() $snippet = $this->snippetFromMethod(Bytes::class, 'type'); $snippet->addLocal('bytes', $this->bytes); $res = $snippet->invoke(); - $this->assertEquals(ValueMapper::TYPE_BYTES, $res->output()); + $this->assertEquals(Database::TYPE_BYTES, $res->output()); } public function testFormatAsString() diff --git a/tests/snippets/Spanner/DatabaseTest.php b/tests/snippets/Spanner/DatabaseTest.php index 14a58795a8d6..76156f66209f 100644 --- a/tests/snippets/Spanner/DatabaseTest.php +++ b/tests/snippets/Spanner/DatabaseTest.php @@ -337,7 +337,7 @@ public function testRunTransaction() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] @@ -573,7 +573,7 @@ public function testExecute() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] @@ -602,7 +602,7 @@ public function testExecuteBeginSnapshot() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] @@ -636,7 +636,7 @@ public function testExecuteBeginTransaction() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] @@ -664,7 +664,7 @@ public function testExecuteWithParameterType() $this->connection->executeStreamingSql(Argument::that(function ($arg) { if (!isset($arg['params'])) return false; if (!isset($arg['paramTypes'])) return false; - if ($arg['paramTypes']['timestamp']['code'] !== ValueMapper::TYPE_TIMESTAMP) return false; + if ($arg['paramTypes']['timestamp']['code'] !== Database::TYPE_TIMESTAMP) return false; return true; }))->shouldBeCalled()->willReturn($this->resultGenerator([ @@ -674,7 +674,7 @@ public function testExecuteWithParameterType() [ 'name' => 'lastModifiedTime', 'type' => [ - 'code' => ValueMapper::TYPE_TIMESTAMP + 'code' => Database::TYPE_TIMESTAMP ] ] ] @@ -688,7 +688,7 @@ public function testExecuteWithParameterType() $snippet = $this->snippetFromMethod(Database::class, 'execute', 3); $snippet->addLocal('database', $this->database); $snippet->addLocal('timestamp', null); - $snippet->addUse(ValueMapper::class); + $snippet->addUse(Database::class); $res = $snippet->invoke('neverEditedPosts'); $this->assertNull($res->returnVal()->current()['lastModifiedTime']); @@ -699,8 +699,8 @@ public function testExecuteWithEmptyArray() $this->connection->executeStreamingSql(Argument::that(function ($arg) { if (!isset($arg['params'])) return false; if (!isset($arg['paramTypes'])) return false; - if ($arg['paramTypes']['emptyArrayOfIntegers']['code'] !== ValueMapper::TYPE_ARRAY) return false; - if ($arg['paramTypes']['emptyArrayOfIntegers']['arrayElementType']['code'] !== ValueMapper::TYPE_INT64) return false; + if ($arg['paramTypes']['emptyArrayOfIntegers']['code'] !== Database::TYPE_ARRAY) return false; + if ($arg['paramTypes']['emptyArrayOfIntegers']['arrayElementType']['code'] !== Database::TYPE_INT64) return false; return true; }))->shouldBeCalled()->willReturn($this->resultGenerator([ @@ -710,9 +710,9 @@ public function testExecuteWithEmptyArray() [ 'name' => 'numbers', 'type' => [ - 'code' => ValueMapper::TYPE_ARRAY, + 'code' => Database::TYPE_ARRAY, 'arrayElementType' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] @@ -726,7 +726,7 @@ public function testExecuteWithEmptyArray() $snippet = $this->snippetFromMethod(Database::class, 'execute', 4); $snippet->addLocal('database', $this->database); - $snippet->addUse(ValueMapper::class); + $snippet->addUse(Database::class); $res = $snippet->invoke('emptyArray'); $this->assertEmpty($res->returnVal()); @@ -743,7 +743,7 @@ public function testRead() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] @@ -773,7 +773,7 @@ public function testReadWithSnapshot() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] @@ -808,7 +808,7 @@ public function testReadWithTransaction() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] diff --git a/tests/snippets/Spanner/DateTest.php b/tests/snippets/Spanner/DateTest.php index 0ac50dc03239..0e9b5450b373 100644 --- a/tests/snippets/Spanner/DateTest.php +++ b/tests/snippets/Spanner/DateTest.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Tests\Snippets\Spanner; use Google\Cloud\Dev\Snippet\SnippetTestCase; +use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\ValueMapper; @@ -78,7 +79,7 @@ public function testType() $snippet = $this->snippetFromMethod(Date::class, 'type'); $snippet->addLocal('date', $this->date); $res = $snippet->invoke(); - $this->assertEquals(ValueMapper::TYPE_DATE, $res->output()); + $this->assertEquals(Database::TYPE_DATE, $res->output()); } public function testFormatAsString() diff --git a/tests/snippets/Spanner/SnapshotTest.php b/tests/snippets/Spanner/SnapshotTest.php index 52779a8aeac8..c0147d2f49ed 100644 --- a/tests/snippets/Spanner/SnapshotTest.php +++ b/tests/snippets/Spanner/SnapshotTest.php @@ -97,7 +97,7 @@ public function testExecute() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] @@ -126,7 +126,7 @@ public function testRead() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] diff --git a/tests/snippets/Spanner/TimestampTest.php b/tests/snippets/Spanner/TimestampTest.php index 1d51dcba2d53..9ad3f85165ed 100644 --- a/tests/snippets/Spanner/TimestampTest.php +++ b/tests/snippets/Spanner/TimestampTest.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Tests\Snippets\Spanner; use Google\Cloud\Dev\Snippet\SnippetTestCase; +use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\ValueMapper; @@ -69,7 +70,7 @@ public function testType() $snippet->addLocal('timestamp', $this->timestamp); $res = $snippet->invoke('type'); - $this->assertEquals(ValueMapper::TYPE_TIMESTAMP, $res->returnVal()); + $this->assertEquals(Database::TYPE_TIMESTAMP, $res->returnVal()); } public function testFormatAsString() diff --git a/tests/snippets/Spanner/TransactionTest.php b/tests/snippets/Spanner/TransactionTest.php index 7514fd2e490f..72fc4f38389e 100644 --- a/tests/snippets/Spanner/TransactionTest.php +++ b/tests/snippets/Spanner/TransactionTest.php @@ -316,7 +316,7 @@ private function resultGenerator() [ 'name' => 'loginCount', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] diff --git a/tests/system/Spanner/QueryTest.php b/tests/system/Spanner/QueryTest.php index 72a73fc57652..b1e10e0b5208 100644 --- a/tests/system/Spanner/QueryTest.php +++ b/tests/system/Spanner/QueryTest.php @@ -19,9 +19,9 @@ use Google\Cloud\Core\Int64; use Google\Cloud\Spanner\Bytes; +use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\Timestamp; -use Google\Cloud\Spanner\ValueMapper; /** * @group spanner @@ -105,7 +105,7 @@ public function testBindBoolParameterNull() 'param' => null ], 'types' => [ - 'param' => ValueMapper::TYPE_BOOL + 'param' => Database::TYPE_BOOL ] ]); @@ -159,7 +159,7 @@ public function testBindNullIntParameter() 'param' => null ], 'types' => [ - 'param' => ValueMapper::TYPE_INT64 + 'param' => Database::TYPE_INT64 ] ]); @@ -197,7 +197,7 @@ public function testBindFloat64ParameterNull() 'param' => null ], 'types' => [ - 'param' => ValueMapper::TYPE_FLOAT64 + 'param' => Database::TYPE_FLOAT64 ] ]); @@ -235,7 +235,7 @@ public function testBindStringParameterNull() 'param' => null ], 'types' => [ - 'param' => ValueMapper::TYPE_STRING + 'param' => Database::TYPE_STRING ] ]); @@ -276,7 +276,7 @@ public function testBindBytesParameterNull() 'param' => null ], 'types' => [ - 'param' => ValueMapper::TYPE_BYTES + 'param' => Database::TYPE_BYTES ] ]); @@ -316,7 +316,7 @@ public function testBindTimestampParameterNull() 'param' => null ], 'types' => [ - 'param' => ValueMapper::TYPE_TIMESTAMP + 'param' => Database::TYPE_TIMESTAMP ] ]); @@ -356,7 +356,7 @@ public function testBindDateParameterNull() 'param' => null ], 'types' => [ - 'param' => ValueMapper::TYPE_DATE + 'param' => Database::TYPE_DATE ] ]); @@ -416,7 +416,7 @@ public function testBindEmptyArrayOfType($type) 'param' => [] ], 'types' => [ - 'param' => [ValueMapper::TYPE_ARRAY, $type] + 'param' => [Database::TYPE_ARRAY, $type] ] ]); @@ -444,7 +444,7 @@ public function testBindNullArrayOfType($type) 'param' => null ], 'types' => [ - 'param' => [ValueMapper::TYPE_ARRAY, $type] + 'param' => [Database::TYPE_ARRAY, $type] ] ]); @@ -586,26 +586,26 @@ function (array $res) { public function arrayTypesEmpty() { return [ - [ValueMapper::TYPE_BOOL], - [ValueMapper::TYPE_INT64], - [ValueMapper::TYPE_FLOAT64], - [ValueMapper::TYPE_STRING], - [ValueMapper::TYPE_BYTES], - [ValueMapper::TYPE_TIMESTAMP], - [ValueMapper::TYPE_DATE], + [Database::TYPE_BOOL], + [Database::TYPE_INT64], + [Database::TYPE_FLOAT64], + [Database::TYPE_STRING], + [Database::TYPE_BYTES], + [Database::TYPE_TIMESTAMP], + [Database::TYPE_DATE], ]; } public function arrayTypesNull() { return [ - [ValueMapper::TYPE_BOOL], - [ValueMapper::TYPE_INT64], - [ValueMapper::TYPE_FLOAT64], - [ValueMapper::TYPE_STRING], - [ValueMapper::TYPE_BYTES], - [ValueMapper::TYPE_TIMESTAMP], - [ValueMapper::TYPE_DATE], + [Database::TYPE_BOOL], + [Database::TYPE_INT64], + [Database::TYPE_FLOAT64], + [Database::TYPE_STRING], + [Database::TYPE_BYTES], + [Database::TYPE_TIMESTAMP], + [Database::TYPE_DATE], ]; } } diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php index 6ea000dd4063..57b45a0133ee 100644 --- a/tests/unit/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/DatabaseTest.php @@ -658,7 +658,7 @@ private function resultGenerator() [ 'name' => 'ID', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php index aa4eae0a2ac0..e1437da9aebe 100644 --- a/tests/unit/Spanner/OperationTest.php +++ b/tests/unit/Spanner/OperationTest.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Connection\ConnectionInterface; +use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; @@ -165,7 +166,7 @@ public function testExecute() if ($arg['sql'] !== $sql) return false; if ($arg['session'] !== self::SESSION) return false; if ($arg['params'] !== ['id' => '10']) return false; - if ($arg['paramTypes']['id']['code'] !== ValueMapper::TYPE_INT64) return false; + if ($arg['paramTypes']['id']['code'] !== Database::TYPE_INT64) return false; return true; }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); @@ -311,7 +312,7 @@ private function executeAndReadResponse(array $additionalMetadata = []) [ 'name' => 'ID', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php index eabceddf61bb..744b3d337021 100644 --- a/tests/unit/Spanner/TransactionTest.php +++ b/tests/unit/Spanner/TransactionTest.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Tests\Unit\Spanner; use Google\Cloud\Spanner\Connection\ConnectionInterface; +use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; @@ -313,7 +314,7 @@ private function resultGenerator() [ 'name' => 'ID', 'type' => [ - 'code' => ValueMapper::TYPE_INT64 + 'code' => Database::TYPE_INT64 ] ] ] diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php index e59def509444..83f9efcf10a9 100644 --- a/tests/unit/Spanner/ValueMapperTest.php +++ b/tests/unit/Spanner/ValueMapperTest.php @@ -19,6 +19,7 @@ use Google\Cloud\Core\Int64; use Google\Cloud\Spanner\Bytes; +use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\Result; use Google\Cloud\Spanner\Timestamp; @@ -50,10 +51,10 @@ public function testFormatParamsForExecuteSqlSimpleTypes() $res = $this->mapper->formatParamsForExecuteSql($params); $this->assertEquals($params, $res['params']); - $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['id']['code']); - $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['name']['code']); - $this->assertEquals(ValueMapper::TYPE_FLOAT64, $res['paramTypes']['pi']['code']); - $this->assertEquals(ValueMapper::TYPE_BOOL, $res['paramTypes']['isCool']['code']); + $this->assertEquals(Database::TYPE_INT64, $res['paramTypes']['id']['code']); + $this->assertEquals(Database::TYPE_STRING, $res['paramTypes']['name']['code']); + $this->assertEquals(Database::TYPE_FLOAT64, $res['paramTypes']['pi']['code']); + $this->assertEquals(Database::TYPE_BOOL, $res['paramTypes']['isCool']['code']); } public function testFormatParamsForExecuteSqlResource() @@ -71,7 +72,7 @@ public function testFormatParamsForExecuteSqlResource() $res = $this->mapper->formatParamsForExecuteSql($params); $this->assertEquals($c, base64_decode($res['params']['resource'])); - $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['resource']['code']); + $this->assertEquals(Database::TYPE_BYTES, $res['paramTypes']['resource']['code']); } public function testFormatParamsForExecuteSqlArray() @@ -84,8 +85,8 @@ public function testFormatParamsForExecuteSqlArray() $this->assertEquals('foo', $res['params']['array'][0]); $this->assertEquals('bar', $res['params']['array'][1]); - $this->assertEquals(ValueMapper::TYPE_ARRAY, $res['paramTypes']['array']['code']); - $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['array']['arrayElementType']['code']); + $this->assertEquals(Database::TYPE_ARRAY, $res['paramTypes']['array']['code']); + $this->assertEquals(Database::TYPE_STRING, $res['paramTypes']['array']['arrayElementType']['code']); } /** @@ -116,7 +117,7 @@ public function testFormatParamsForExecuteSqlInt64() $res = $this->mapper->formatParamsForExecuteSql($params); $this->assertEquals($val, $res['params']['int']); - $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['int']['code']); + $this->assertEquals(Database::TYPE_INT64, $res['paramTypes']['int']['code']); } public function testFormatParamsForExecuteSqlValueInterface() @@ -128,7 +129,7 @@ public function testFormatParamsForExecuteSqlValueInterface() $res = $this->mapper->formatParamsForExecuteSql($params); $this->assertEquals($val, base64_decode($res['params']['bytes'])); - $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['bytes']['code']); + $this->assertEquals(Database::TYPE_BYTES, $res['paramTypes']['bytes']['code']); } /** @@ -180,7 +181,7 @@ public function testEncodeValuesAsSimpleType() public function testDecodeValuesThrowsExceptionWithInvalidFormat() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_STRING), + $this->createField(Database::TYPE_STRING), $this->createRow(self::FORMAT_TEST_VALUE), 'Not a real format' ); @@ -192,7 +193,7 @@ public function testDecodeValuesThrowsExceptionWithInvalidFormat() public function testDecodeValuesReturnsVariedFormats($expectedOutput, $format) { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_STRING), + $this->createField(Database::TYPE_STRING), $this->createRow(self::FORMAT_TEST_VALUE), $format ); @@ -228,7 +229,7 @@ public function formatProvider() public function testDecodeValuesBool() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_BOOL), + $this->createField(Database::TYPE_BOOL), $this->createRow(false), Result::RETURN_ASSOCIATIVE ); @@ -238,7 +239,7 @@ public function testDecodeValuesBool() public function testDecodeValuesInt() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_INT64), + $this->createField(Database::TYPE_INT64), $this->createRow('555'), Result::RETURN_ASSOCIATIVE ); @@ -249,7 +250,7 @@ public function testDecodeValuesInt64Object() { $mapper = new ValueMapper(true); $res = $mapper->decodeValues( - $this->createField(ValueMapper::TYPE_INT64), + $this->createField(Database::TYPE_INT64), $this->createRow('555'), Result::RETURN_ASSOCIATIVE ); @@ -260,7 +261,7 @@ public function testDecodeValuesInt64Object() public function testDecodeValuesFloat() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createField(Database::TYPE_FLOAT64), $this->createRow(3.1415), Result::RETURN_ASSOCIATIVE ); @@ -270,7 +271,7 @@ public function testDecodeValuesFloat() public function testDecodeValuesFloatNaN() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createField(Database::TYPE_FLOAT64), $this->createRow('NaN'), Result::RETURN_ASSOCIATIVE ); @@ -280,7 +281,7 @@ public function testDecodeValuesFloatNaN() public function testDecodeValuesFloatInfinity() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createField(Database::TYPE_FLOAT64), $this->createRow('Infinity'), Result::RETURN_ASSOCIATIVE ); @@ -292,7 +293,7 @@ public function testDecodeValuesFloatInfinity() public function testDecodeValuesFloatNegativeInfinity() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createField(Database::TYPE_FLOAT64), $this->createRow('-Infinity'), Result::RETURN_ASSOCIATIVE ); @@ -307,7 +308,7 @@ public function testDecodeValuesFloatNegativeInfinity() public function testDecodeValuesFloatError() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createField(Database::TYPE_FLOAT64), $this->createRow('foo'), Result::RETURN_ASSOCIATIVE ); @@ -316,7 +317,7 @@ public function testDecodeValuesFloatError() public function testDecodeValuesString() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_STRING), + $this->createField(Database::TYPE_STRING), $this->createRow('foo'), Result::RETURN_ASSOCIATIVE ); @@ -327,7 +328,7 @@ public function testDecodeValuesTimestamp() { $dt = new \DateTime; $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_TIMESTAMP), + $this->createField(Database::TYPE_TIMESTAMP), $this->createRow($dt->format(Timestamp::FORMAT)), Result::RETURN_ASSOCIATIVE ); @@ -340,7 +341,7 @@ public function testDecodeValuesDate() { $dt = new \DateTime; $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_DATE), + $this->createField(Database::TYPE_DATE), $this->createRow($dt->format(Date::FORMAT)), Result::RETURN_ASSOCIATIVE ); @@ -352,7 +353,7 @@ public function testDecodeValuesDate() public function testDecodeValuesBytes() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_BYTES), + $this->createField(Database::TYPE_BYTES), $this->createRow(base64_encode('hello world')), Result::RETURN_ASSOCIATIVE ); @@ -364,8 +365,8 @@ public function testDecodeValuesBytes() public function testDecodeValuesArray() { $res = $this->mapper->decodeValues( - $this->createField(ValueMapper::TYPE_ARRAY, 'arrayElementType', [ - 'code' => ValueMapper::TYPE_STRING + $this->createField(Database::TYPE_ARRAY, 'arrayElementType', [ + 'code' => Database::TYPE_STRING ]), $this->createRow(['foo', 'bar']), Result::RETURN_ASSOCIATIVE @@ -380,15 +381,15 @@ public function testDecodeValuesStruct() $field = [ 'name' => 'structTest', 'type' => [ - 'code' => ValueMapper::TYPE_ARRAY, + 'code' => Database::TYPE_ARRAY, 'arrayElementType' => [ - 'code' => ValueMapper::TYPE_STRUCT, + 'code' => Database::TYPE_STRUCT, 'structType' => [ 'fields' => [ [ 'name' => 'rowName', 'type' => [ - 'code' => ValueMapper::TYPE_STRING + 'code' => Database::TYPE_STRING ] ] ] @@ -418,11 +419,11 @@ public function testDecodeValuesAnonymousField() [ 'name' => 'ID', 'type' => [ - 'code' => ValueMapper::TYPE_INT64, + 'code' => Database::TYPE_INT64, ] ], [ 'type' => [ - 'code' => ValueMapper::TYPE_STRING + 'code' => Database::TYPE_STRING ] ] ]; From f0856c7160715006af6c9c1a697a880860a5afb7 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Fri, 16 Jun 2017 11:53:57 -0700 Subject: [PATCH 43/46] Skip creating the labels info until there are labels. (#542) Revert extension name change in test --- src/Trace/TraceSpan.php | 6 +++++- tests/unit/Trace/TraceSpanTest.php | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Trace/TraceSpan.php b/src/Trace/TraceSpan.php index 664a5e882643..c9c5e27b5aa1 100644 --- a/src/Trace/TraceSpan.php +++ b/src/Trace/TraceSpan.php @@ -67,7 +67,7 @@ class TraceSpan implements \JsonSerializable public function __construct($options = []) { $this->info = $this->pluckArray( - ['spanId', 'kind', 'name', 'parentSpanId', 'labels'], + ['spanId', 'kind', 'name', 'parentSpanId'], $options ); @@ -78,6 +78,10 @@ public function __construct($options = []) $this->setEnd($options['endTime']); } + if (array_key_exists('labels', $options)) { + $this->addLabels($options['labels']); + } + $this->info += [ 'kind' => self::SPAN_KIND_UNSPECIFIED ]; diff --git a/tests/unit/Trace/TraceSpanTest.php b/tests/unit/Trace/TraceSpanTest.php index 349ecec99c11..35757c3556a3 100644 --- a/tests/unit/Trace/TraceSpanTest.php +++ b/tests/unit/Trace/TraceSpanTest.php @@ -67,6 +67,13 @@ public function testNoLabels() $this->assertArrayNotHasKey('labels', $info); } + public function testEmptyLabels() + { + $traceSpan = new TraceSpan(['labels' => []]); + $info = $traceSpan->info(); + $this->assertArrayNotHasKey('labels', $info); + } + public function testGeneratesDefaultSpanName() { $traceSpan = new TraceSpan(); From 62dcea05232b35040370772a17e4f33da85c7778 Mon Sep 17 00:00:00 2001 From: Brent Shaffer Date: Mon, 19 Jun 2017 08:28:10 -0700 Subject: [PATCH 44/46] upgrade to google/auth ^1.0 (#535) * upgrade to google/auth ^1.0 * Update google/gax dependency --- composer.json | 4 ++-- src/Core/composer.json | 2 +- src/ErrorReporting/composer.json | 2 +- src/Monitoring/composer.json | 2 +- src/Spanner/composer.json | 2 +- src/VideoIntelligence/composer.json | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index fc697823f244..48082a55d256 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "require": { "php": ">=5.5", "rize/uri-template": "~0.3", - "google/auth": "^0.11", + "google/auth": "^1.0", "guzzlehttp/guzzle": "^5.3|^6.0", "guzzlehttp/psr7": "^1.2", "monolog/monolog": "~1", @@ -58,7 +58,7 @@ "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", "google/proto-client-php": "^0.13", - "google/gax": "^0.9", + "google/gax": "^0.10", "symfony/lock": "3.3.x-dev#1ba6ac9" }, "suggest": { diff --git a/src/Core/composer.json b/src/Core/composer.json index d7b094add539..d05ecb4f0908 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -6,7 +6,7 @@ "require": { "php": ">=5.5", "rize/uri-template": "~0.3", - "google/auth": "^0.11", + "google/auth": "^1.0", "guzzlehttp/guzzle": "^5.3|^6.0", "guzzlehttp/psr7": "^1.2", "monolog/monolog": "~1", diff --git a/src/ErrorReporting/composer.json b/src/ErrorReporting/composer.json index 96a2fdb0fcce..d8ad6f6255f6 100644 --- a/src/ErrorReporting/composer.json +++ b/src/ErrorReporting/composer.json @@ -6,7 +6,7 @@ "require": { "ext-grpc": "*", "google/proto-client-php": "^0.13", - "google/gax": "^0.9" + "google/gax": "^0.10" }, "extra": { "component": { diff --git a/src/Monitoring/composer.json b/src/Monitoring/composer.json index 1343c2173513..e0694f2e52dd 100644 --- a/src/Monitoring/composer.json +++ b/src/Monitoring/composer.json @@ -6,7 +6,7 @@ "require": { "ext-grpc": "*", "google/proto-client-php": "^0.13", - "google/gax": "^0.9" + "google/gax": "^0.10" }, "extra": { "component": { diff --git a/src/Spanner/composer.json b/src/Spanner/composer.json index b9ff6d27761c..6ed869dcfb88 100644 --- a/src/Spanner/composer.json +++ b/src/Spanner/composer.json @@ -6,7 +6,7 @@ "require": { "ext-grpc": "*", "google/cloud-core": "^1.4", - "google/gax": "^0.9", + "google/gax": "^0.10", "google/proto-client-php": "^0.13" }, "suggest": { diff --git a/src/VideoIntelligence/composer.json b/src/VideoIntelligence/composer.json index b2730781b126..e4c329818d4a 100644 --- a/src/VideoIntelligence/composer.json +++ b/src/VideoIntelligence/composer.json @@ -6,7 +6,7 @@ "require": { "ext-grpc": "*", "google/proto-client-php": "^0.13", - "google/gax": "^0.9" + "google/gax": "^0.10" }, "extra": { "component": { From 71e349230c6e486e6a7dd5a7eed7253176445cc7 Mon Sep 17 00:00:00 2001 From: Ryan Gordon Date: Mon, 19 Jun 2017 11:20:17 -0700 Subject: [PATCH 45/46] Fixing a documentation comment in SpannerClient class construct (#543) --- src/Spanner/SpannerClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 92d60e364b11..8b10231f16c9 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -91,7 +91,7 @@ class SpannerClient * @type callable $authHttpHandler A handler used to deliver Psr7 * requests specifically for authentication. * @type callable $httpHandler A handler used to deliver Psr7 requests. - * @type string $keyFile The contents of the service account + * @type array $keyFile The json decoded contents of the service account * credentials .json file retrieved from the Google Developers * Console. * @type string $keyFilePath The full path to your service account From f0ec0210170736297456d7ba7ab0d615b027959f Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Mon, 19 Jun 2017 15:39:52 -0400 Subject: [PATCH 46/46] Add support for Storage - Requester Pays (#527) * Add support for Storage - Requester Pays * Add system tests for requester pays * Address code review * Fix StorageObject::exists() * Fix system test for storage IAM * Add whitelist notice --- src/Storage/Bucket.php | 42 +- .../ServiceDefinition/storage-v1.json | 1283 ++++++++--------- src/Storage/StorageClient.php | 25 +- src/Storage/StorageObject.php | 33 +- tests/system/Storage/ManageBucketsTest.php | 2 +- tests/system/Storage/RequesterPaysTest.php | 182 +++ tests/system/Storage/StorageTestCase.php | 5 +- tests/unit/Storage/BucketTest.php | 14 +- tests/unit/Storage/StorageObjectTest.php | 16 +- 9 files changed, 851 insertions(+), 751 deletions(-) create mode 100644 tests/system/Storage/RequesterPaysTest.php diff --git a/src/Storage/Bucket.php b/src/Storage/Bucket.php index a4e81a97cf60..98f501e0766e 100644 --- a/src/Storage/Bucket.php +++ b/src/Storage/Bucket.php @@ -82,10 +82,13 @@ class Bucket * @param string $name The bucket's name. * @param array $info [optional] The bucket's metadata. */ - public function __construct(ConnectionInterface $connection, $name, array $info = null) + public function __construct(ConnectionInterface $connection, $name, array $info = []) { $this->connection = $connection; - $this->identity = ['bucket' => $name]; + $this->identity = [ + 'bucket' => $name, + 'userProject' => $this->pluck('requesterProjectId', $info, false) + ]; $this->info = $info; $this->acl = new Acl($this->connection, 'bucketAccessControls', $this->identity); $this->defaultAcl = new Acl($this->connection, 'defaultObjectAccessControls', $this->identity); @@ -244,8 +247,7 @@ public function upload($data, array $options = []) $encryptionKeySHA256 = isset($options['encryptionKeySHA256']) ? $options['encryptionKeySHA256'] : null; $response = $this->connection->insertObject( - $this->formatEncryptionHeaders($options) + [ - 'bucket' => $this->identity['bucket'], + $this->formatEncryptionHeaders($options) + $this->identity + [ 'data' => $data ] )->upload(); @@ -318,8 +320,7 @@ public function getResumableUploader($data, array $options = []) } return $this->connection->insertObject( - $this->formatEncryptionHeaders($options) + [ - 'bucket' => $this->identity['bucket'], + $this->formatEncryptionHeaders($options) + $this->identity + [ 'data' => $data, 'resumable' => true ] @@ -384,8 +385,7 @@ public function getStreamableUploader($data, array $options = []) } return $this->connection->insertObject( - $this->formatEncryptionHeaders($options) + [ - 'bucket' => $this->identity['bucket'], + $this->formatEncryptionHeaders($options) + $this->identity + [ 'data' => $data, 'streamable' => true, 'validate' => false @@ -430,7 +430,9 @@ public function object($name, array $options = []) $name, $this->identity['bucket'], $generation, - null, + array_filter([ + 'requesterProjectId' => $this->identity['userProject'] + ]), $encryptionKey, $encryptionKeySHA256 ); @@ -491,7 +493,9 @@ function (array $object) { $object['name'], $this->identity['bucket'], isset($object['generation']) ? $object['generation'] : null, - $object + $object + array_filter([ + 'requesterProjectId' => $this->identity['userProject'] + ]) ); }, [$this->connection, 'listObjects'], @@ -581,6 +585,9 @@ public function delete(array $options = []) * `"STANDARD"` and `"DURABLE_REDUCED_AVAILABILITY"`. * @type array $versioning The bucket's versioning configuration. * @type array $website The bucket's website configuration. + * @type array $billing The bucket's billing configuration. **Whitelist + * Warning:** At the time of publication, this argument is subject + * to a feature whitelist and may not be available in your project. * @type array $labels The Bucket labels. Labels are represented as an * array of keys and values. To remove an existing label, set its * value to `null`. @@ -647,6 +654,7 @@ public function compose(array $sourceObjects, $name, array $options = []) 'destinationObject' => $name, 'destinationPredefinedAcl' => isset($options['predefinedAcl']) ? $options['predefinedAcl'] : null, 'destination' => isset($options['metadata']) ? $options['metadata'] : null, + 'userProject' => $this->identity['userProject'], 'sourceObjects' => array_map(function ($sourceObject) { $name = null; $generation = null; @@ -683,7 +691,9 @@ public function compose(array $sourceObjects, $name, array $options = []) $response['name'], $this->identity['bucket'], $response['generation'], - $response + $response + array_filter([ + 'requesterProjectId' => $this->identity['userProject'] + ]) ); } @@ -715,11 +725,7 @@ public function compose(array $sourceObjects, $name, array $options = []) */ public function info(array $options = []) { - if (!$this->info) { - $this->reload($options); - } - - return $this->info; + return $this->info ?: $this->reload($options); } /** @@ -825,9 +831,7 @@ public function iam() $this->identity['bucket'], [ 'parent' => null, - 'args' => [ - 'bucket' => $this->identity['bucket'] - ] + 'args' => $this->identity ] ); } diff --git a/src/Storage/Connection/ServiceDefinition/storage-v1.json b/src/Storage/Connection/ServiceDefinition/storage-v1.json index 7e8342a53e43..9a4901781672 100644 --- a/src/Storage/Connection/ServiceDefinition/storage-v1.json +++ b/src/Storage/Connection/ServiceDefinition/storage-v1.json @@ -1,11 +1,11 @@ { "kind": "discovery#restDescription", - "etag": "\"tbys6C40o18GZwyMen5GMkdK-3s/z5yry82uiiVbT8ZegvP6VogRacg\"", + "etag": "\"YWOzh2SDasdU84ArJnpYek-OMdg/GRtDpiDRivRW7hDEFVrFuoXBCxk\"", "discoveryVersion": "v1", "id": "storage:v1", "name": "storage", "version": "v1", - "revision": "20170224", + "revision": "20170510", "title": "Cloud Storage JSON API", "description": "Stores and retrieves potentially large, immutable data objects.", "ownerDomain": "google.com", @@ -15,9 +15,7 @@ "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png" }, "documentationLink": "https://developers.google.com/storage/docs/json_api/", - "labels": [ - "labs" - ], + "labels": ["labs"], "protocol": "rest", "baseUrl": "https://www.googleapis.com/storage/v1/", "basePath": "/storage/v1/", @@ -29,12 +27,8 @@ "type": "string", "description": "Data format for the response.", "default": "json", - "enum": [ - "json" - ], - "enumDescriptions": [ - "Responses with Content-Type of application/json" - ], + "enum": ["json"], + "enumDescriptions": ["Responses with Content-Type of application/json"], "location": "query" }, "fields": { @@ -103,9 +97,17 @@ "$ref": "BucketAccessControl" }, "annotations": { - "required": [ - "storage.buckets.update" - ] + "required": ["storage.buckets.update"] + } + }, + "billing": { + "type": "object", + "description": "The bucket's billing configuration.", + "properties": { + "requesterPays": { + "type": "boolean", + "description": "When set to true, bucket is requester pays." + } } }, "cors": { @@ -259,9 +261,7 @@ "type": "string", "description": "The name of the bucket.", "annotations": { - "required": [ - "storage.buckets.insert" - ] + "required": ["storage.buckets.insert"] } }, "owner": { @@ -348,9 +348,7 @@ "type": "string", "description": "The entity holding the permission, in one of the following forms: \n- user-userId \n- user-email \n- group-groupId \n- group-email \n- domain-domain \n- project-team-projectId \n- allUsers \n- allAuthenticatedUsers Examples: \n- The user liz@example.com would be user-liz@example.com. \n- The group example@googlegroups.com would be group-example@googlegroups.com. \n- To refer to all members of the Google Apps for Business domain example.com, the entity would be domain-example.com.", "annotations": { - "required": [ - "storage.bucketAccessControls.insert" - ] + "required": ["storage.bucketAccessControls.insert"] } }, "entityId": { @@ -388,9 +386,7 @@ "type": "string", "description": "The access permission for the entity.", "annotations": { - "required": [ - "storage.bucketAccessControls.insert" - ] + "required": ["storage.bucketAccessControls.insert"] } }, "selfLink": { @@ -523,9 +519,7 @@ "type": "string", "description": "The source object's name. The source object's bucket is implicitly the destination bucket.", "annotations": { - "required": [ - "storage.objects.compose" - ] + "required": ["storage.objects.compose"] } }, "objectPreconditions": { @@ -542,13 +536,84 @@ } }, "annotations": { - "required": [ - "storage.objects.compose" - ] + "required": ["storage.objects.compose"] } } } }, + "Notification": { + "id": "Notification", + "type": "object", + "description": "A subscription to receive Google PubSub notifications.", + "properties": { + "custom_attributes": { + "type": "object", + "description": "An optional list of additional attributes to attach to each Cloud PubSub message published for this notification subscription.", + "additionalProperties": { + "type": "string" + } + }, + "etag": { + "type": "string", + "description": "HTTP 1.1 Entity tag for this subscription notification." + }, + "event_types": { + "type": "array", + "description": "If present, only send notifications about listed event types. If empty, sent notifications for all event types.", + "items": { + "type": "string" + } + }, + "id": { + "type": "string", + "description": "The ID of the notification." + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For notifications, this is always storage#notification.", + "default": "storage#notification" + }, + "object_name_prefix": { + "type": "string", + "description": "If present, only apply this notification configuration to object names that begin with this prefix." + }, + "payload_format": { + "type": "string", + "description": "The desired content of the Payload.", + "default": "JSON_API_V1" + }, + "selfLink": { + "type": "string", + "description": "The canonical URL of this notification." + }, + "topic": { + "type": "string", + "description": "The Cloud PubSub topic to which this subscription publishes. Formatted as: '//pubsub.googleapis.com/projects/{project-identifier}/topics/{my-topic}'", + "annotations": { + "required": ["storage.notifications.insert"] + } + } + } + }, + "Notifications": { + "id": "Notifications", + "type": "object", + "description": "A list of notification subscriptions.", + "properties": { + "items": { + "type": "array", + "description": "The list of items.", + "items": { + "$ref": "Notification" + } + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For lists of notifications, this is always storage#notifications.", + "default": "storage#notifications" + } + } + }, "Object": { "id": "Object", "type": "object", @@ -561,9 +626,7 @@ "$ref": "ObjectAccessControl" }, "annotations": { - "required": [ - "storage.objects.update" - ] + "required": ["storage.objects.update"] } }, "bucket": { @@ -726,10 +789,7 @@ "type": "string", "description": "The entity holding the permission, in one of the following forms: \n- user-userId \n- user-email \n- group-groupId \n- group-email \n- domain-domain \n- project-team-projectId \n- allUsers \n- allAuthenticatedUsers Examples: \n- The user liz@example.com would be user-liz@example.com. \n- The group example@googlegroups.com would be group-example@googlegroups.com. \n- To refer to all members of the Google Apps for Business domain example.com, the entity would be domain-example.com.", "annotations": { - "required": [ - "storage.defaultObjectAccessControls.insert", - "storage.objectAccessControls.insert" - ] + "required": ["storage.defaultObjectAccessControls.insert", "storage.objectAccessControls.insert"] } }, "entityId": { @@ -776,10 +836,7 @@ "type": "string", "description": "The access permission for the entity.", "annotations": { - "required": [ - "storage.defaultObjectAccessControls.insert", - "storage.objectAccessControls.insert" - ] + "required": ["storage.defaultObjectAccessControls.insert", "storage.objectAccessControls.insert"] } }, "selfLink": { @@ -855,29 +912,20 @@ "type": "string" }, "annotations": { - "required": [ - "storage.buckets.setIamPolicy", - "storage.objects.setIamPolicy" - ] + "required": ["storage.buckets.setIamPolicy", "storage.objects.setIamPolicy"] } }, "role": { "type": "string", "description": "The role to which members belong. Two types of roles are supported: new IAM roles, which grant permissions that do not map directly to those provided by ACLs, and legacy IAM roles, which do map directly to ACL permissions. All roles are of the format roles/storage.specificRole.\nThe new IAM roles are: \n- roles/storage.admin — Full control of Google Cloud Storage resources. \n- roles/storage.objectViewer — Read-Only access to Google Cloud Storage objects. \n- roles/storage.objectCreator — Access to create objects in Google Cloud Storage. \n- roles/storage.objectAdmin — Full control of Google Cloud Storage objects. The legacy IAM roles are: \n- roles/storage.legacyObjectReader — Read-only access to objects without listing. Equivalent to an ACL entry on an object with the READER role. \n- roles/storage.legacyObjectOwner — Read/write access to existing objects without listing. Equivalent to an ACL entry on an object with the OWNER role. \n- roles/storage.legacyBucketReader — Read access to buckets with object listing. Equivalent to an ACL entry on a bucket with the READER role. \n- roles/storage.legacyBucketWriter — Read access to buckets with object listing/creation/deletion. Equivalent to an ACL entry on a bucket with the WRITER role. \n- roles/storage.legacyBucketOwner — Read and write access to existing buckets with object listing/creation/deletion. Equivalent to an ACL entry on a bucket with the OWNER role.", "annotations": { - "required": [ - "storage.buckets.setIamPolicy", - "storage.objects.setIamPolicy" - ] + "required": ["storage.buckets.setIamPolicy", "storage.objects.setIamPolicy"] } } } }, "annotations": { - "required": [ - "storage.buckets.setIamPolicy", - "storage.objects.setIamPolicy" - ] + "required": ["storage.buckets.setIamPolicy", "storage.objects.setIamPolicy"] } }, "etag": { @@ -913,7 +961,7 @@ "objectSize": { "type": "string", "description": "The total size of the object being copied in bytes. This property is always present in the response.", - "format": "uint64" + "format": "int64" }, "resource": { "$ref": "Object", @@ -926,7 +974,23 @@ "totalBytesRewritten": { "type": "string", "description": "The total bytes written so far, which can be used to provide a waiting user with a progress indicator. This property is always present in the response.", - "format": "uint64" + "format": "int64" + } + } + }, + "ServiceAccount": { + "id": "ServiceAccount", + "type": "object", + "description": "A subscription to receive Google PubSub notifications.", + "properties": { + "email_address": { + "type": "string", + "description": "The ID of the notification." + }, + "kind": { + "type": "string", + "description": "The kind of item this is. For notifications, this is always storage#notification.", + "default": "storage#serviceAccount" } } }, @@ -970,16 +1034,15 @@ "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "entity" - ], - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "parameterOrder": ["bucket", "entity"], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "get": { "id": "storage.bucketAccessControls.get", @@ -998,19 +1061,18 @@ "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "entity" - ], + "parameterOrder": ["bucket", "entity"], "response": { "$ref": "BucketAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "insert": { "id": "storage.bucketAccessControls.insert", @@ -1023,21 +1085,21 @@ "description": "Name of a bucket.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "request": { "$ref": "BucketAccessControl" }, "response": { "$ref": "BucketAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "list": { "id": "storage.bucketAccessControls.list", @@ -1050,18 +1112,18 @@ "description": "Name of a bucket.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "response": { "$ref": "BucketAccessControls" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "patch": { "id": "storage.bucketAccessControls.patch", @@ -1080,22 +1142,21 @@ "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "entity" - ], + "parameterOrder": ["bucket", "entity"], "request": { "$ref": "BucketAccessControl" }, "response": { "$ref": "BucketAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "update": { "id": "storage.bucketAccessControls.update", @@ -1114,22 +1175,21 @@ "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "entity" - ], + "parameterOrder": ["bucket", "entity"], "request": { "$ref": "BucketAccessControl" }, "response": { "$ref": "BucketAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] } } }, @@ -1158,16 +1218,15 @@ "description": "If set, only deletes the bucket if its metageneration does not match this value.", "format": "int64", "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket" - ], - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "parameterOrder": ["bucket"], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"] }, "get": { "id": "storage.buckets.get", @@ -1196,30 +1255,21 @@ "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit owner, acl and defaultObjectAcl properties." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit owner, acl and defaultObjectAcl properties."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "response": { "$ref": "Bucket" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] }, "getIamPolicy": { "id": "storage.buckets.getIamPolicy", @@ -1232,21 +1282,18 @@ "description": "Name of a bucket.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "response": { "$ref": "Policy" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] }, "insert": { "id": "storage.buckets.insert", @@ -1257,41 +1304,15 @@ "predefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to this bucket.", - "enum": [ - "authenticatedRead", - "private", - "projectPrivate", - "publicRead", - "publicReadWrite" - ], - "enumDescriptions": [ - "Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", - "Project team owners get OWNER access.", - "Project team members get access according to their roles.", - "Project team owners get OWNER access, and allUsers get READER access.", - "Project team owners get OWNER access, and allUsers get WRITER access." - ], + "enum": ["authenticatedRead", "private", "projectPrivate", "publicRead", "publicReadWrite"], + "enumDescriptions": ["Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", "Project team owners get OWNER access.", "Project team members get access according to their roles.", "Project team owners get OWNER access, and allUsers get READER access.", "Project team owners get OWNER access, and allUsers get WRITER access."], "location": "query" }, "predefinedDefaultObjectAcl": { "type": "string", "description": "Apply a predefined set of default object access controls to this bucket.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "project": { @@ -1303,31 +1324,19 @@ "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl, unless the bucket resource specifies acl or defaultObjectAcl properties, when it defaults to full.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit owner, acl and defaultObjectAcl properties." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit owner, acl and defaultObjectAcl properties."], "location": "query" } }, - "parameterOrder": [ - "project" - ], + "parameterOrder": ["project"], "request": { "$ref": "Bucket" }, "response": { "$ref": "Bucket" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"] }, "list": { "id": "storage.buckets.list", @@ -1362,30 +1371,16 @@ "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit owner, acl and defaultObjectAcl properties." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit owner, acl and defaultObjectAcl properties."], "location": "query" } }, - "parameterOrder": [ - "project" - ], + "parameterOrder": ["project"], "response": { "$ref": "Buckets" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] }, "patch": { "id": "storage.buckets.patch", @@ -1414,70 +1409,38 @@ "predefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to this bucket.", - "enum": [ - "authenticatedRead", - "private", - "projectPrivate", - "publicRead", - "publicReadWrite" - ], - "enumDescriptions": [ - "Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", - "Project team owners get OWNER access.", - "Project team members get access according to their roles.", - "Project team owners get OWNER access, and allUsers get READER access.", - "Project team owners get OWNER access, and allUsers get WRITER access." - ], + "enum": ["authenticatedRead", "private", "projectPrivate", "publicRead", "publicReadWrite"], + "enumDescriptions": ["Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", "Project team owners get OWNER access.", "Project team members get access according to their roles.", "Project team owners get OWNER access, and allUsers get READER access.", "Project team owners get OWNER access, and allUsers get WRITER access."], "location": "query" }, "predefinedDefaultObjectAcl": { "type": "string", "description": "Apply a predefined set of default object access controls to this bucket.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "projection": { "type": "string", "description": "Set of properties to return. Defaults to full.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit owner, acl and defaultObjectAcl properties." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit owner, acl and defaultObjectAcl properties."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "request": { "$ref": "Bucket" }, "response": { "$ref": "Bucket" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "setIamPolicy": { "id": "storage.buckets.setIamPolicy", @@ -1490,22 +1453,21 @@ "description": "Name of a bucket.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "request": { "$ref": "Policy" }, "response": { "$ref": "Policy" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"] }, "testIamPermissions": { "id": "storage.buckets.testIamPermissions", @@ -1525,22 +1487,18 @@ "required": true, "repeated": true, "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "permissions" - ], + "parameterOrder": ["bucket", "permissions"], "response": { "$ref": "TestIamPermissionsResponse" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] }, "update": { "id": "storage.buckets.update", @@ -1569,70 +1527,38 @@ "predefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to this bucket.", - "enum": [ - "authenticatedRead", - "private", - "projectPrivate", - "publicRead", - "publicReadWrite" - ], - "enumDescriptions": [ - "Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", - "Project team owners get OWNER access.", - "Project team members get access according to their roles.", - "Project team owners get OWNER access, and allUsers get READER access.", - "Project team owners get OWNER access, and allUsers get WRITER access." - ], + "enum": ["authenticatedRead", "private", "projectPrivate", "publicRead", "publicReadWrite"], + "enumDescriptions": ["Project team owners get OWNER access, and allAuthenticatedUsers get READER access.", "Project team owners get OWNER access.", "Project team members get access according to their roles.", "Project team owners get OWNER access, and allUsers get READER access.", "Project team owners get OWNER access, and allUsers get WRITER access."], "location": "query" }, "predefinedDefaultObjectAcl": { "type": "string", "description": "Apply a predefined set of default object access controls to this bucket.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "projection": { "type": "string", "description": "Set of properties to return. Defaults to full.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit owner, acl and defaultObjectAcl properties." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit owner, acl and defaultObjectAcl properties."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "request": { "$ref": "Bucket" }, "response": { "$ref": "Bucket" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] } } }, @@ -1647,13 +1573,7 @@ "$ref": "Channel", "parameterName": "resource" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] } } }, @@ -1676,16 +1596,15 @@ "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "entity" - ], - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "parameterOrder": ["bucket", "entity"], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "get": { "id": "storage.defaultObjectAccessControls.get", @@ -1704,19 +1623,18 @@ "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "entity" - ], + "parameterOrder": ["bucket", "entity"], "response": { "$ref": "ObjectAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "insert": { "id": "storage.defaultObjectAccessControls.insert", @@ -1729,21 +1647,21 @@ "description": "Name of a bucket.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "request": { "$ref": "ObjectAccessControl" }, "response": { "$ref": "ObjectAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "list": { "id": "storage.defaultObjectAccessControls.list", @@ -1768,18 +1686,18 @@ "description": "If present, only return default ACL listing if the bucket's current metageneration does not match the given value.", "format": "int64", "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "response": { "$ref": "ObjectAccessControls" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "patch": { "id": "storage.defaultObjectAccessControls.patch", @@ -1798,22 +1716,21 @@ "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "entity" - ], + "parameterOrder": ["bucket", "entity"], "request": { "$ref": "ObjectAccessControl" }, "response": { "$ref": "ObjectAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "update": { "id": "storage.defaultObjectAccessControls.update", @@ -1832,22 +1749,133 @@ "description": "The entity holding the permission. Can be user-userId, user-emailAddress, group-groupId, group-emailAddress, allUsers, or allAuthenticatedUsers.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "entity" - ], + "parameterOrder": ["bucket", "entity"], "request": { "$ref": "ObjectAccessControl" }, "response": { "$ref": "ObjectAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] + } + } + }, + "notifications": { + "methods": { + "delete": { + "id": "storage.notifications.delete", + "path": "b/{bucket}/notificationConfigs/{notification}", + "httpMethod": "DELETE", + "description": "Permanently deletes a notification subscription.", + "parameters": { + "bucket": { + "type": "string", + "description": "The parent bucket of the notification.", + "required": true, + "location": "path" + }, + "notification": { + "type": "string", + "description": "ID of the notification to delete.", + "required": true, + "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" + } + }, + "parameterOrder": ["bucket", "notification"], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"] + }, + "get": { + "id": "storage.notifications.get", + "path": "b/{bucket}/notificationConfigs/{notification}", + "httpMethod": "GET", + "description": "View a notification configuration.", + "parameters": { + "bucket": { + "type": "string", + "description": "The parent bucket of the notification.", + "required": true, + "location": "path" + }, + "notification": { + "type": "string", + "description": "Notification ID", + "required": true, + "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" + } + }, + "parameterOrder": ["bucket", "notification"], + "response": { + "$ref": "Notification" + }, + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] + }, + "insert": { + "id": "storage.notifications.insert", + "path": "b/{bucket}/notificationConfigs", + "httpMethod": "POST", + "description": "Creates a notification subscription for a given bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "The parent bucket of the notification.", + "required": true, + "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" + } + }, + "parameterOrder": ["bucket"], + "request": { + "$ref": "Notification" + }, + "response": { + "$ref": "Notification" + }, + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"] + }, + "list": { + "id": "storage.notifications.list", + "path": "b/{bucket}/notificationConfigs", + "httpMethod": "GET", + "description": "Retrieves a list of notification subscriptions for a given bucket.", + "parameters": { + "bucket": { + "type": "string", + "description": "Name of a GCS bucket.", + "required": true, + "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" + } + }, + "parameterOrder": ["bucket"], + "response": { + "$ref": "Notifications" + }, + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] } } }, @@ -1882,17 +1910,15 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object", - "entity" - ], - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "parameterOrder": ["bucket", "object", "entity"], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "get": { "id": "storage.objectAccessControls.get", @@ -1923,20 +1949,18 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object", - "entity" - ], + "parameterOrder": ["bucket", "object", "entity"], "response": { "$ref": "ObjectAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "insert": { "id": "storage.objectAccessControls.insert", @@ -1961,22 +1985,21 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object" - ], + "parameterOrder": ["bucket", "object"], "request": { "$ref": "ObjectAccessControl" }, "response": { "$ref": "ObjectAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "list": { "id": "storage.objectAccessControls.list", @@ -2001,19 +2024,18 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object" - ], + "parameterOrder": ["bucket", "object"], "response": { "$ref": "ObjectAccessControls" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "patch": { "id": "storage.objectAccessControls.patch", @@ -2044,23 +2066,21 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object", - "entity" - ], + "parameterOrder": ["bucket", "object", "entity"], "request": { "$ref": "ObjectAccessControl" }, "response": { "$ref": "ObjectAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "update": { "id": "storage.objectAccessControls.update", @@ -2091,23 +2111,21 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object", - "entity" - ], + "parameterOrder": ["bucket", "object", "entity"], "request": { "$ref": "ObjectAccessControl" }, "response": { "$ref": "ObjectAccessControl" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] } } }, @@ -2134,22 +2152,8 @@ "destinationPredefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to the destination object.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "ifGenerationMatch": { @@ -2163,23 +2167,21 @@ "description": "Makes the operation conditional on whether the object's current metageneration matches the given value.", "format": "int64", "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "destinationBucket", - "destinationObject" - ], + "parameterOrder": ["destinationBucket", "destinationObject"], "request": { "$ref": "ComposeRequest" }, "response": { "$ref": "Object" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"], "supportsMediaDownload": true, "useMediaDownloadService": true }, @@ -2204,22 +2206,8 @@ "destinationPredefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to the destination object.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "ifGenerationMatch": { @@ -2273,14 +2261,8 @@ "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl, unless the object resource specifies the acl property, when it defaults to full.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit the owner, acl property." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit the owner, acl property."], "location": "query" }, "sourceBucket": { @@ -2300,25 +2282,21 @@ "description": "Name of the source object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "sourceBucket", - "sourceObject", - "destinationBucket", - "destinationObject" - ], + "parameterOrder": ["sourceBucket", "sourceObject", "destinationBucket", "destinationObject"], "request": { "$ref": "Object" }, "response": { "$ref": "Object" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"], "supportsMediaDownload": true, "useMediaDownloadService": true }, @@ -2369,17 +2347,15 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object" - ], - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "parameterOrder": ["bucket", "object"], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"] }, "get": { "id": "storage.objects.get", @@ -2432,31 +2408,21 @@ "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit the owner, acl property." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit the owner, acl property."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" } }, - "parameterOrder": [ - "bucket", - "object" - ], + "parameterOrder": ["bucket", "object"], "response": { "$ref": "Object" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"], "supportsMediaDownload": true, "useMediaDownloadService": true }, @@ -2483,22 +2449,18 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object" - ], + "parameterOrder": ["bucket", "object"], "response": { "$ref": "Policy" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] }, "insert": { "id": "storage.objects.insert", @@ -2549,59 +2511,36 @@ "predefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to this object.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl, unless the object resource specifies the acl property, when it defaults to full.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit the owner, acl property." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit the owner, acl property."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "request": { "$ref": "Object" }, "response": { "$ref": "Object" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"], "supportsMediaDownload": true, "useMediaDownloadService": true, "supportsMediaUpload": true, "mediaUpload": { - "accept": [ - "*/*" - ], + "accept": ["*/*"], "protocols": { "simple": { "multipart": true, @@ -2652,14 +2591,13 @@ "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit the owner, acl property." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit the owner, acl property."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" }, "versions": { @@ -2668,19 +2606,11 @@ "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "response": { "$ref": "Objects" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"], "supportsSubscription": true }, "patch": { @@ -2734,52 +2664,31 @@ "predefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to this object.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "projection": { "type": "string", "description": "Set of properties to return. Defaults to full.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit the owner, acl property." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit the owner, acl property."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" } }, - "parameterOrder": [ - "bucket", - "object" - ], + "parameterOrder": ["bucket", "object"], "request": { "$ref": "Object" }, "response": { "$ref": "Object" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"] }, "rewrite": { "id": "storage.objects.rewrite", @@ -2802,22 +2711,8 @@ "destinationPredefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to the destination object.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "ifGenerationMatch": { @@ -2877,14 +2772,8 @@ "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl, unless the object resource specifies the acl property, when it defaults to full.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit the owner, acl property." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit the owner, acl property."], "location": "query" }, "rewriteToken": { @@ -2909,25 +2798,21 @@ "description": "Name of the source object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "sourceBucket", - "sourceObject", - "destinationBucket", - "destinationObject" - ], + "parameterOrder": ["sourceBucket", "sourceObject", "destinationBucket", "destinationObject"], "request": { "$ref": "Object" }, "response": { "$ref": "RewriteResponse" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"] }, "setIamPolicy": { "id": "storage.objects.setIamPolicy", @@ -2952,23 +2837,21 @@ "description": "Name of the object. For information about how to URL encode object names to be path safe, see Encoding URI Path Parts.", "required": true, "location": "path" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object" - ], + "parameterOrder": ["bucket", "object"], "request": { "$ref": "Policy" }, "response": { "$ref": "Policy" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_write"] }, "testIamPermissions": { "id": "storage.objects.testIamPermissions", @@ -3000,23 +2883,18 @@ "required": true, "repeated": true, "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", + "location": "query" } }, - "parameterOrder": [ - "bucket", - "object", - "permissions" - ], + "parameterOrder": ["bucket", "object", "permissions"], "response": { "$ref": "TestIamPermissionsResponse" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ] + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] }, "update": { "id": "storage.objects.update", @@ -3069,52 +2947,31 @@ "predefinedAcl": { "type": "string", "description": "Apply a predefined set of access controls to this object.", - "enum": [ - "authenticatedRead", - "bucketOwnerFullControl", - "bucketOwnerRead", - "private", - "projectPrivate", - "publicRead" - ], - "enumDescriptions": [ - "Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", - "Object owner gets OWNER access, and project team owners get OWNER access.", - "Object owner gets OWNER access, and project team owners get READER access.", - "Object owner gets OWNER access.", - "Object owner gets OWNER access, and project team members get access according to their roles.", - "Object owner gets OWNER access, and allUsers get READER access." - ], + "enum": ["authenticatedRead", "bucketOwnerFullControl", "bucketOwnerRead", "private", "projectPrivate", "publicRead"], + "enumDescriptions": ["Object owner gets OWNER access, and allAuthenticatedUsers get READER access.", "Object owner gets OWNER access, and project team owners get OWNER access.", "Object owner gets OWNER access, and project team owners get READER access.", "Object owner gets OWNER access.", "Object owner gets OWNER access, and project team members get access according to their roles.", "Object owner gets OWNER access, and allUsers get READER access."], "location": "query" }, "projection": { "type": "string", "description": "Set of properties to return. Defaults to full.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit the owner, acl property." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit the owner, acl property."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" } }, - "parameterOrder": [ - "bucket", - "object" - ], + "parameterOrder": ["bucket", "object"], "request": { "$ref": "Object" }, "response": { "$ref": "Object" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control" - ], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/devstorage.full_control"], "supportsMediaDownload": true, "useMediaDownloadService": true }, @@ -3156,14 +3013,13 @@ "projection": { "type": "string", "description": "Set of properties to return. Defaults to noAcl.", - "enum": [ - "full", - "noAcl" - ], - "enumDescriptions": [ - "Include all properties.", - "Omit the owner, acl property." - ], + "enum": ["full", "noAcl"], + "enumDescriptions": ["Include all properties.", "Omit the owner, acl property."], + "location": "query" + }, + "userProject": { + "type": "string", + "description": "The project number to be billed for this request, for Requester Pays buckets.", "location": "query" }, "versions": { @@ -3172,9 +3028,7 @@ "location": "query" } }, - "parameterOrder": [ - "bucket" - ], + "parameterOrder": ["bucket"], "request": { "$ref": "Channel", "parameterName": "resource" @@ -3182,16 +3036,37 @@ "response": { "$ref": "Channel" }, - "scopes": [ - "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/cloud-platform.read-only", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_only", - "https://www.googleapis.com/auth/devstorage.read_write" - ], + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"], "supportsSubscription": true } } + }, + "projects": { + "resources": { + "serviceAccount": { + "methods": { + "get": { + "id": "storage.projects.serviceAccount.get", + "path": "projects/{projectId}/serviceAccount", + "httpMethod": "GET", + "description": "Get the email address of this project's GCS service account.", + "parameters": { + "projectId": { + "type": "string", + "description": "Project ID", + "required": true, + "location": "path" + } + }, + "parameterOrder": ["projectId"], + "response": { + "$ref": "ServiceAccount" + }, + "scopes": ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/cloud-platform.read-only", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/devstorage.read_only", "https://www.googleapis.com/auth/devstorage.read_write"] + } + } + } + } } } } diff --git a/src/Storage/StorageClient.php b/src/Storage/StorageClient.php index 7ed9835201ef..16eb7484df99 100644 --- a/src/Storage/StorageClient.php +++ b/src/Storage/StorageClient.php @@ -93,17 +93,32 @@ public function __construct(array $config = []) * point. To see the operations that can be performed on a bucket please * see {@see Google\Cloud\Storage\Bucket}. * + * If `$requesterPays` is set to true, the current project ID (used to + * instantiate the client) will be billed for all requests. If + * `$requesterPays` is a project ID, given as a string, that project + * will be billed for all requests. This only has an effect when the bucket + * is not owned by the current or given project ID. + * * Example: * ``` * $bucket = $storage->bucket('my-bucket'); * ``` * * @param string $name The name of the bucket to request. + * @param string|bool $requesterPays If true, the current Project ID + * will be used. If a string, that string will be used as the userProject + * argument. **Defaults to** `false`. * @return Bucket */ - public function bucket($name) + public function bucket($name, $requesterPays = false) { - return new Bucket($this->connection, $name); + if (!$requesterPays) { + $requesterPays = null; + } elseif (!is_string($requesterPays)) { + $requesterPays = $this->projectId; + } + + return new Bucket($this->connection, $name, ['requesterProjectId' => $requesterPays]); } /** @@ -217,6 +232,12 @@ function (array $bucket) { * **Defaults to** `STANDARD`. * @type array $versioning The bucket's versioning configuration. * @type array $website The bucket's website configuration. + * @type array $billing The bucket's billing configuration. **Whitelist + * Warning:** At the time of publication, this argument is subject + * to a feature whitelist and may not be available in your project. + * @type array $labels The Bucket labels. Labels are represented as an + * array of keys and values. To remove an existing label, set its + * value to `null`. * } * @return Bucket */ diff --git a/src/Storage/StorageObject.php b/src/Storage/StorageObject.php index 79f38860479c..750e6e1d93cb 100644 --- a/src/Storage/StorageObject.php +++ b/src/Storage/StorageObject.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Storage; +use Google\Cloud\Core\ArrayTrait; use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Storage\Connection\ConnectionInterface; use GuzzleHttp\Psr7; @@ -38,6 +39,7 @@ */ class StorageObject { + use ArrayTrait; use EncryptionTrait; /** @@ -82,7 +84,7 @@ public function __construct( $name, $bucket, $generation = null, - array $info = null, + array $info = [], $encryptionKey = null, $encryptionKeySHA256 = null ) { @@ -95,7 +97,8 @@ public function __construct( $this->identity = [ 'bucket' => $bucket, 'object' => $name, - 'generation' => $generation + 'generation' => $generation, + 'userProject' => $this->pluck('requesterProjectId', $info, false) ]; $this->acl = new Acl($this->connection, 'objectAccessControls', $this->identity); } @@ -127,12 +130,13 @@ public function acl() * } * ``` * + * @param array $options [optional] Configuration options. * @return bool */ - public function exists() + public function exists(array $options = []) { try { - $this->connection->getObject($this->identity + ['fields' => 'name']); + $this->connection->getObject($this->identity + $options + ['fields' => 'name']); } catch (NotFoundException $ex) { return false; } @@ -170,7 +174,7 @@ public function exists() */ public function delete(array $options = []) { - $this->connection->deleteObject($options + $this->identity); + $this->connection->deleteObject($options + array_filter($this->identity)); } /** @@ -226,7 +230,7 @@ public function update(array $metadata, array $options = []) $options['acl'] = null; } - return $this->info = $this->connection->patchObject($options + $this->identity); + return $this->info = $this->connection->patchObject($options + array_filter($this->identity)); } /** @@ -313,7 +317,7 @@ public function copy($destination, array $options = []) $response['name'], $response['bucket'], $response['generation'], - $response, + $response + ['requesterProjectId' => $this->identity['userProject']], $key, $keySHA256 ); @@ -442,7 +446,7 @@ public function rewrite($destination, array $options = []) $response['resource']['name'], $response['resource']['bucket'], $response['resource']['generation'], - $response['resource'], + $response['resource'] + ['requesterProjectId' => $this->identity['userProject']], $destinationKey, $destinationKeySHA256 ); @@ -623,7 +627,7 @@ public function downloadAsStream(array $options = []) $this->formatEncryptionHeaders( $options + $this->encryptionData - + $this->identity + + array_filter($this->identity) ) ); } @@ -672,11 +676,7 @@ public function downloadAsStream(array $options = []) */ public function info(array $options = []) { - if (!$this->info) { - $this->reload($options); - } - - return $this->info; + return $this->info ?: $this->reload($options); } /** @@ -725,7 +725,7 @@ public function reload(array $options = []) $this->formatEncryptionHeaders( $options + $this->encryptionData - + $this->identity + + array_filter($this->identity) ) ); } @@ -807,7 +807,8 @@ private function formatDestinationRequest($destination, array $options) 'destinationPredefinedAcl' => $destAcl, 'sourceBucket' => $this->identity['bucket'], 'sourceObject' => $this->identity['object'], - 'sourceGeneration' => $this->identity['generation'] + 'sourceGeneration' => $this->identity['generation'], + 'userProject' => $this->identity['userProject'], ]) + $this->formatEncryptionHeaders($options + $this->encryptionData); } } diff --git a/tests/system/Storage/ManageBucketsTest.php b/tests/system/Storage/ManageBucketsTest.php index 7079fe7633f3..0d98e9009b8f 100644 --- a/tests/system/Storage/ManageBucketsTest.php +++ b/tests/system/Storage/ManageBucketsTest.php @@ -100,7 +100,7 @@ public function testIam() $resourceId = explode('#', $policy['resourceId'])[0]; $bucketName = self::$bucket->name(); - $this->assertEquals($resourceId, sprintf('buckets/%s', $bucketName)); + $this->assertEquals($resourceId, sprintf('projects/_/buckets/%s', $bucketName)); $role = 'roles/storage.admin'; diff --git a/tests/system/Storage/RequesterPaysTest.php b/tests/system/Storage/RequesterPaysTest.php new file mode 100644 index 000000000000..2661168bbcf3 --- /dev/null +++ b/tests/system/Storage/RequesterPaysTest.php @@ -0,0 +1,182 @@ +createBucket(self::$bucketName, [ + 'billing' => ['requesterPays' => true] + ]); + + self::$object1 = self::$ownerBucketInstance->upload( + fopen(self::$path, 'r') + ); + + self::$object2 = self::$ownerBucketInstance->upload( + fopen(self::$path, 'r') + ); + + self::$deletionQueue[] = self::$object1; + self::$deletionQueue[] = self::$object2; + self::$deletionQueue[] = self::$ownerBucketInstance; + } + + public function setUp() + { + if (!defined('GOOGLE_CLOUD_WHITELIST_KEY_PATH')) { + $this->markTestSkipped('Missing whitelist keyfile path for whitelist system tests.'); + } + + $this->keyFilePath = GOOGLE_CLOUD_WHITELIST_KEY_PATH; + $this->requesterPaysClient = new StorageClient([ + 'keyFilePath' => $this->keyFilePath + ]); + } + + /** + * @dataProvider requesterPaysMethods + * @expectedException Google\Cloud\Core\Exception\BadRequestException + */ + public function testRequesterPaysMethodsWithoutUserProject(callable $call) + { + $bucket = $this->requesterPaysClient->bucket(self::$bucketName); + $object = $bucket->object(self::$object1->name()); + + $call($bucket, $object); + } + + public function requesterPaysMethods() + { + return [ + [ + function (Bucket $bucket) { + $bucket->exists(); + }, + ], [ + function (Bucket $bucket) { + $bucket->upload( + fopen(self::$path, 'r') + ); + }, + ], [ + function (Bucket $bucket) { + $bucket->getResumableUploader( + fopen(self::$path, 'r') + )->upload(); + }, + ], [ + function (Bucket $bucket) { + $bucket->getStreamableUploader( + fopen(self::$path, 'r') + )->upload(); + }, + ], [ + function (Bucket $bucket) { + iterator_to_array($bucket->objects()); + }, + ], [ + function (Bucket $bucket) { + $bucket->update([]); + }, + ], [ + function (Bucket $bucket) { + $bucket->compose([self::$object1, self::$object2], uniqid(self::TESTING_PREFIX), [ + 'metadata' => ['contentType' => 'text/plain'] + ]); + }, + ], [ + function (Bucket $bucket) { + $bucket->reload(); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $object->exists(); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $object->update([]); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $object->copy($bucket); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $object->rewrite($bucket); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $object->downloadAsString(); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $object->downloadToFile('php://temp'); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $object->downloadAsStream(); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $object->reload(); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $bucket->iam()->policy(); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $bucket->iam()->setPolicy([]); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $bucket->iam()->testPermissions(['foo']); + } + ], [ + function (Bucket $bucket, StorageObject $object) { + $bucket->iam()->reload(); + } + ] + ]; + } +} diff --git a/tests/system/Storage/StorageTestCase.php b/tests/system/Storage/StorageTestCase.php index 89020b46f2bd..954d2577f052 100644 --- a/tests/system/Storage/StorageTestCase.php +++ b/tests/system/Storage/StorageTestCase.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Tests\System\Storage; +use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\ExponentialBackoff; use Google\Cloud\Storage\StorageClient; @@ -59,7 +60,9 @@ public static function tearDownFixtures() foreach (self::$deletionQueue as $item) { $backoff->execute(function () use ($item) { - $item->delete(); + try { + $item->delete(); + } catch (NotFoundException $e) {} }); } } diff --git a/tests/unit/Storage/BucketTest.php b/tests/unit/Storage/BucketTest.php index 1c3ff9ea1e32..7a8a5f935c81 100644 --- a/tests/unit/Storage/BucketTest.php +++ b/tests/unit/Storage/BucketTest.php @@ -222,11 +222,11 @@ public function testComposesObjects( $destinationBucket = 'bucket'; $destinationObject = 'combined-files.txt'; $this->connection->composeObject([ + 'destinationBucket' => $destinationBucket, + 'destinationObject' => $destinationObject, 'destinationPredefinedAcl' => $acl, 'destination' => $metadata + ['contentType' => 'text/plain'], 'sourceObjects' => $expectedSourceObjects, - 'destinationBucket' => $destinationBucket, - 'destinationObject' => $destinationObject ]) ->willReturn([ 'name' => $destinationObject, @@ -363,4 +363,14 @@ public function testIam() $this->assertInstanceOf(Iam::class, $bucket->iam()); } + + public function testRequesterPays() + { + $this->connection->getBucket(Argument::withEntry('userProject', 'foo')) + ->willReturn([]); + + $bucket = new Bucket($this->connection->reveal(), 'bucket', ['requesterProjectId' => 'foo']); + + $bucket->reload(); + } } diff --git a/tests/unit/Storage/StorageObjectTest.php b/tests/unit/Storage/StorageObjectTest.php index c385a2c7dd5c..2896b4355679 100644 --- a/tests/unit/Storage/StorageObjectTest.php +++ b/tests/unit/Storage/StorageObjectTest.php @@ -95,7 +95,6 @@ public function testUpdatesDataAndUnsetsAclWithPredefinedAclApplied() $this->connection->patchObject($predefinedAcl + [ 'bucket' => $bucket, 'object' => $object, - 'generation' => null, 'acl' => null ])->willReturn([]); $object = new StorageObject( @@ -347,7 +346,6 @@ public function testDownloadsAsString() $this->connection->downloadObject([ 'bucket' => $bucket, 'object' => $object, - 'generation' => null, 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', @@ -377,7 +375,6 @@ public function testDownloadsToFile() $this->connection->downloadObject([ 'bucket' => $bucket, 'object' => $object, - 'generation' => null, 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', @@ -406,7 +403,6 @@ public function testGetBodyWithoutExtraOptions() $this->connection->downloadObject([ 'bucket' => $bucket, 'object' => $object, - 'generation' => null, ]) ->willReturn($stream); @@ -428,7 +424,6 @@ public function testGetBodyWithExtraOptions() $this->connection->downloadObject([ 'bucket' => $bucket, 'object' => $object, - 'generation' => null, 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', @@ -478,7 +473,6 @@ public function testGetsInfoWithReload() $this->connection->getObject([ 'bucket' => $bucket, 'object' => $object, - 'generation' => null, 'restOptions' => [ 'headers' => [ 'x-goog-encryption-algorithm' => 'AES256', @@ -519,4 +513,14 @@ public function testGetsGcsUri() $expectedUri = sprintf('gs://%s/%s', $bucketName, $name); $this->assertEquals($expectedUri, $object->gcsUri()); } + + public function testRequesterPays() + { + $this->connection->getObject(Argument::withEntry('userProject', 'foo')) + ->willReturn([]); + + $object = new StorageObject($this->connection->reveal(), 'object', 'bucket', null, ['requesterProjectId' => 'foo']); + + $object->reload(); + } }