diff --git a/.github/run-package-tests.sh b/.github/run-package-tests.sh index 8e55d0ca9934..a02cfa1dbe1b 100644 --- a/.github/run-package-tests.sh +++ b/.github/run-package-tests.sh @@ -15,6 +15,20 @@ set -e +# USAGE: +# +# run-package-tests.sh [DIRECTORY] [PREFER_LOWEST] +# +# DIRECTORY: Optionally pass in a component directory and only run the script +#. for that component. +# +# PREFER_LOWEST: can be "--prefer-lowest" or "--prefer-lowest-strict". When the +# "--prefer-lowest-strict" flag is set, local package dependencies +# are installed according to their version number in +# `[Component]/VERSION`. This flag is set on the release PRs to +#. ensure the dependencies for the upcoming release will be +# configured correctly. + DIRS=$(find * -maxdepth 0 -type d -name '[A-Z]*') PREFER_LOWEST="" if [ "$#" -eq 1 ]; then diff --git a/.github/workflows/emulator-system-tests-spanner.yaml b/.github/workflows/emulator-system-tests-spanner.yaml index 3b321c9b8f64..72fb75408a96 100644 --- a/.github/workflows/emulator-system-tests-spanner.yaml +++ b/.github/workflows/emulator-system-tests-spanner.yaml @@ -49,7 +49,7 @@ jobs: - name: Install dependencies run: | # ensure composer uses local Core instead of pulling from packagist - composer config repositories.local --json '{"type":"path", "url": "../Core", "options": {"versions": {"google/cloud-core": "1.100"}}}' -d Spanner + composer config repositories.local --json '{"type":"path", "url": "../Core", "options": {"versions": {"google/cloud-core": "1.100"}},"canonical":false}' -d Spanner composer update --prefer-dist --no-interaction --no-suggest -d Spanner/ - name: Run system tests diff --git a/Core/src/ApiHelperTrait.php b/Core/src/ApiHelperTrait.php index c4b352944897..5ef652d06f7e 100644 --- a/Core/src/ApiHelperTrait.php +++ b/Core/src/ApiHelperTrait.php @@ -21,6 +21,7 @@ use Google\ApiCore\Options\CallOptions; use Google\Protobuf\Internal\Message; use Google\Protobuf\NullValue; +use LogicException; /** * @internal @@ -276,6 +277,8 @@ private function splitOptionalArgs(array $input, array $extraAllowedKeys = []): * $optionTypes can be an array of string keys, a protobuf Message classname, or a * the CallOptions classname. Parameters are split and returned in the order * that the options types are provided. + * + * @throws LogicException */ private function validateOptions(array $options, array|Message|string ...$optionTypes): array { diff --git a/Core/src/Iam/Iam.php b/Core/src/Iam/Iam.php index 3d530d8a3fc2..cd351934da6d 100644 --- a/Core/src/Iam/Iam.php +++ b/Core/src/Iam/Iam.php @@ -34,7 +34,7 @@ * * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $instance = $spanner->instance('my-new-instance'); * * $iam = $instance->iam(); diff --git a/Core/src/LongRunning/LongRunningClientConnection.php b/Core/src/LongRunning/LongRunningClientConnection.php new file mode 100644 index 000000000000..037a5d1ffa8a --- /dev/null +++ b/Core/src/LongRunning/LongRunningClientConnection.php @@ -0,0 +1,123 @@ +gapicClient->resumeOperation($args['name']); + + return $this->operationResponseToArray($operationResponse); + } + + /** + * @param array $args + * @return array + */ + public function cancel(array $args): array + { + $operationResponse = $this->gapicClient->resumeOperation( + $args['name'], + $args['method'] ?? null + ); + $operationResponse->cancel(); + + return $this->operationResponseToArray($operationResponse); + } + + /** + * @param array $args + * @return array + */ + public function delete(array $args): array + { + $operationResponse = $this->gapicClient->resumeOperation( + $args['name'], + $args['method'] ?? null + ); + $operationResponse->cancel(); + + return $this->operationResponseToArray($operationResponse); + } + + /** + * @param array $args + * @return array + */ + public function operations(array $args): array + { + $request = ListOperationsRequest::build($args['name'], $args['filter'] ?? null); + $response = $this->gapicClient->getOperationsClient()->listOperations($request); + + return $this->handleResponse($response); + } + + private function operationResponseToArray(OperationResponse $operationResponse): array + { + $response = $this->handleResponse($operationResponse->getLastProtoResponse()); + $metaType = $response['metadata']['typeUrl']; + + // unpack result Any type + $result = $operationResponse->getResult(); + if ($result instanceof Any) { + // For some reason we aren't doing this in GAX OperationResponse (but we should) + $result = $result->unpack(); + } + $response['response'] = $this->handleResponse($result); + + // unpack error Any type + $response['error'] = $this->handleResponse($operationResponse->getError()); + + $metadata = $operationResponse->getMetadata(); + if ($metadata instanceof Any) { + // For some reason we aren't doing this in GAX OperationResponse (but we should) + $metadata = $metadata->unpack(); + } + $response['metadata'] = $this->handleResponse($metadata); + + // Used in LongRunningOperation to invoke callables + $response['metadata'] += ['typeUrl' => $metaType]; + + return $response; + } +} diff --git a/Core/src/LongRunning/LongRunningOperation.php b/Core/src/LongRunning/LongRunningOperation.php index d0decd5967ec..64c898f065e0 100644 --- a/Core/src/LongRunning/LongRunningOperation.php +++ b/Core/src/LongRunning/LongRunningOperation.php @@ -19,6 +19,7 @@ /** * Represent and interact with a Long Running Operation. + * @template T */ class LongRunningOperation { @@ -180,7 +181,7 @@ public function state(array $options = []) * ``` * * @param array $options [optional] Configuration options. - * @return mixed|null + * @return T|mixed|null */ public function result(array $options = []) { @@ -252,12 +253,11 @@ public function reload(array $options = []) $this->result = null; $this->error = null; - if (isset($res['done']) && $res['done']) { + + if ($res['done'] ?? false && isset($res['metadata']['typeUrl'])) { $type = $res['metadata']['typeUrl']; $this->result = $this->executeDoneCallback($type, $res['response']); - $this->error = (isset($res['error'])) - ? $res['error'] - : null; + $this->error = $res['error'] ?? null; } return $this->info = $res; diff --git a/Core/src/OptionsValidator.php b/Core/src/OptionsValidator.php index 3837f0b066fd..cb82ad4bb1ac 100644 --- a/Core/src/OptionsValidator.php +++ b/Core/src/OptionsValidator.php @@ -36,8 +36,9 @@ class OptionsValidator * @param ?Serializer $serializer use a serializer to decode protobuf messages * instead of calling {@see Message::mergeFromJsonString()}. */ - public function __construct(private ?Serializer $serializer = null) - { + public function __construct( + private ?Serializer $serializer = null + ) { } /** @@ -90,6 +91,8 @@ public function validateOptions(array $options, array|Message|string ...$optionT $optionType->mergeFromJsonString(json_encode($messageOptions, JSON_FORCE_OBJECT)); } $splitOptions[] = $optionType; + } elseif (is_string($optionType)) { + $splitOptions[] = $this->pluck($optionType, $options, false); } else { throw new LogicException(sprintf('Invalid option type: %s', $optionType)); } diff --git a/Core/src/RequestHandler.php b/Core/src/RequestHandler.php index f650b942bfa6..ca4dbee34c57 100644 --- a/Core/src/RequestHandler.php +++ b/Core/src/RequestHandler.php @@ -42,16 +42,16 @@ class RequestHandler */ private Serializer $serializer; - private array $clients; + private array $clients = []; /** * @param Serializer $serializer - * @param array $clientClasses + * @param array $clients * @param array $clientConfig */ public function __construct( Serializer $serializer, - array $clientClasses, + array $clients, array $clientConfig = [] ) { //@codeCoverageIgnoreStart @@ -75,9 +75,12 @@ public function __construct( //@codeCoverageIgnoreEnd // Initialize the client classes and store them in memory - $this->clients = []; - foreach ($clientClasses as $className) { - $this->clients[$className] = new $className($clientConfig); + foreach ($clients as $client) { + if (is_object($client)) { + $this->clients[get_class($client)] = $client; + } else { + $this->clients[$client] = new $client($clientConfig); + } } } diff --git a/Core/src/ServiceBuilder.php b/Core/src/ServiceBuilder.php index a2bf67ea4a3f..6add2cf3011a 100644 --- a/Core/src/ServiceBuilder.php +++ b/Core/src/ServiceBuilder.php @@ -257,7 +257,7 @@ public function pubsub(array $config = []) * * Example: * ``` - * $spanner = $cloud->spanner(); + * $spanner = $cloud->spanner(['projectId' => 'my-project']); * ``` * * @param array $config [optional] { diff --git a/Core/src/TimeTrait.php b/Core/src/TimeTrait.php index ab10cf269ad3..2cd9265161a2 100644 --- a/Core/src/TimeTrait.php +++ b/Core/src/TimeTrait.php @@ -79,12 +79,12 @@ private function formatTimeAsString(\DateTimeInterface $dateTime, $ns) $dateTime = $dateTime->setTimeZone(new \DateTimeZone('UTC')); if ($ns === null) { return $dateTime->format(Timestamp::FORMAT); - } else { - return sprintf( - $dateTime->format(Timestamp::FORMAT_INTERPOLATE), - $this->convertNanoSecondsToFraction($ns) - ); } + + return sprintf( + $dateTime->format(Timestamp::FORMAT_INTERPOLATE), + $this->convertNanoSecondsToFraction($ns) + ); } /** @@ -95,10 +95,10 @@ private function formatTimeAsString(\DateTimeInterface $dateTime, $ns) * $dateTime will be used instead. * @return array */ - private function formatTimeAsArray(\DateTimeInterface $dateTime, $ns) + private function formatTimeAsArray(\DateTimeInterface $dateTime, $ns = null) { if ($ns === null) { - $ns = $dateTime->format('u'); + $ns = $this->convertFractionToNanoSeconds($dateTime->format('u')); } return [ 'seconds' => (int) $dateTime->format('U'), diff --git a/Core/src/Timestamp.php b/Core/src/Timestamp.php index 2ab3eeb36f29..b8988e640484 100644 --- a/Core/src/Timestamp.php +++ b/Core/src/Timestamp.php @@ -17,6 +17,8 @@ namespace Google\Cloud\Core; +use DateTimeInterface; + /** * Represents a Timestamp value. * @@ -85,9 +87,9 @@ public function __construct(\DateTimeInterface $value, $nanoSeconds = null) * $dateTime = $timestamp->get(); * ``` * - * @return \DateTimeInterface + * @return DateTimeInterface */ - public function get() + public function get(): DateTimeInterface { return $this->value; } @@ -102,7 +104,7 @@ public function get() * * @return int */ - public function nanoSeconds() + public function nanoSeconds(): int { return $this->nanoSeconds === null ? (int) $this->value->format('u') * 1000 @@ -119,7 +121,7 @@ public function nanoSeconds() * * @return string */ - public function formatAsString() + public function formatAsString(): string { return $this->formatTimeAsString( $this->value, @@ -143,7 +145,7 @@ public function __toString() * * @return array */ - public function formatForApi() + public function formatForApi(): array { return $this->formatTimeAsArray($this->value, $this->nanoSeconds()); } @@ -155,7 +157,7 @@ public function formatForApi() * @access private */ #[\ReturnTypeWillChange] - public function jsonSerialize() + public function jsonSerialize(): string { return $this->formatAsString(); } diff --git a/Core/tests/Snippet/Iam/IamTest.php b/Core/tests/Snippet/Iam/IamTest.php index 4c9ae4988b61..24283448fe3a 100644 --- a/Core/tests/Snippet/Iam/IamTest.php +++ b/Core/tests/Snippet/Iam/IamTest.php @@ -19,6 +19,7 @@ use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; use Google\Cloud\Core\Iam\Iam; +use Google\Cloud\Core\Iam\IamManager; use Google\Cloud\Core\Iam\IamConnectionInterface; use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Spanner\SpannerClient; @@ -55,7 +56,7 @@ public function testClass() ]); $res = $snippet->invoke('iam'); - $this->assertInstanceOf(Iam::class, $res->returnVal()); + $this->assertInstanceOf(IamManager::class, $res->returnVal()); } public function testPolicy() diff --git a/Core/tests/Unit/ApiHelperTraitTest.php b/Core/tests/Unit/ApiHelperTraitTest.php index d10f8b38692d..13eae477da0f 100644 --- a/Core/tests/Unit/ApiHelperTraitTest.php +++ b/Core/tests/Unit/ApiHelperTraitTest.php @@ -366,6 +366,20 @@ public function validateOptionsProvider() (new MockRequest())->setPageToken('foo1'), ] ], + [ + [ + 'baz' => 'bat', + 'pageToken' => 'foo1', + ], + [ + 'baz', + new MockRequest(), + ], + [ + 'bat', + (new MockRequest())->setPageToken('foo1'), + ] + ], ]; } @@ -381,24 +395,4 @@ public function testValidateOptionsThrowsException() $this->implementation->validateOptions($options, ['foo']); } - - public function testValidateOptionsWithClassnameThrowsException() - { - $this->expectException(LogicException::class); - $this->expectExceptionMessage('Invalid option type: ' . Blob::class); - - $options = [ - 'foo' => 'bar', - 'bar' => 'baz', - ]; - - [$blob, $validated] = $this->implementation->validateOptions( - $options, - Blob::class, - ['foo', 'bar'] - ); - - $this->assertEquals([], $blob); - $this->assertEquals($options, $validated); - } } diff --git a/Core/tests/Unit/Batch/BatchDaemonTraitTest.php b/Core/tests/Unit/Batch/BatchDaemonTraitTest.php index 22d0cd126881..5760498301e2 100644 --- a/Core/tests/Unit/Batch/BatchDaemonTraitTest.php +++ b/Core/tests/Unit/Batch/BatchDaemonTraitTest.php @@ -27,6 +27,8 @@ */ class BatchDaemonTraitTest extends TestCase { + private $impl; + public function setUp(): void { $this->impl = TestHelpers::impl(BatchDaemonTrait::class); diff --git a/Core/tests/Unit/LongRunning/OperationResponseTraitTest.php b/Core/tests/Unit/LongRunning/OperationResponseTraitTest.php index 89c35f11752f..d47b162540d5 100644 --- a/Core/tests/Unit/LongRunning/OperationResponseTraitTest.php +++ b/Core/tests/Unit/LongRunning/OperationResponseTraitTest.php @@ -18,10 +18,10 @@ namespace Google\Cloud\Core\Tests\Unit\LongRunning; use Google\ApiCore\OperationResponse; +use Google\ApiCore\Serializer; use Google\Cloud\Core\LongRunning\OperationResponseTrait; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; -use Google\ApiCore\Serializer; use Prophecy\Argument; use Google\Cloud\Audit\RequestMetadata; use Google\Cloud\Audit\AuthorizationInfo; diff --git a/Core/tests/Unit/ServiceBuilderTest.php b/Core/tests/Unit/ServiceBuilderTest.php index e6e17b383e82..4e75f6a8c70e 100644 --- a/Core/tests/Unit/ServiceBuilderTest.php +++ b/Core/tests/Unit/ServiceBuilderTest.php @@ -23,7 +23,6 @@ use Google\Cloud\Firestore\FirestoreClient; use Google\Cloud\Language\LanguageClient; use Google\Cloud\Logging\LoggingClient; -use Google\Cloud\Spanner\SpannerClient; use Google\Cloud\Storage\StorageClient; use Google\Cloud\Core\Tests\Unit\Fixtures; use GuzzleHttp\Psr7\Response; @@ -157,11 +156,6 @@ public function serviceProvider() ], [ 'language', LanguageClient::class - ], [ - 'spanner', - SpannerClient::class, - [], - [$this, 'checkAndSkipGrpcTests'] ], [ 'storage', StorageClient::class diff --git a/Spanner/MIGRATING.md b/Spanner/MIGRATING.md new file mode 100644 index 000000000000..eeb68d7ecf7c --- /dev/null +++ b/Spanner/MIGRATING.md @@ -0,0 +1,120 @@ +# Migrating Google Spanner from V1 to V2 + +## How to upgrade + +Update your `google/cloud-spanner` dependency to `^2.0`: + +``` +{ + "require": { + "google/cloud-spanner": "^2.0" + } +} +``` + +## Changes + +### Client Options changes + +The following client options are removed/replaced with other options present in +[`ClientOptions`][ClientOptions]. This was done to ensure client options are consistent across all +Google Cloud clients. + +- `authCache` -> Moved to `credentialsConfig.authCache` +- `authCacheOptions` -> Moved to `credentialsConfig.authCacheOptions` +- `FetchAuthTokenInterface` -> Moved to `credentials` +- `keyFile` -> Moved to `credentials` +- `keyFilePath` -> Moved to `credentials` +- `requestTimeout` -> Removed from client options and moved to a call option `timeoutMillis` +- `scopes` -> Moved to `credentialsConfig.scopes` +- `quotaProject` -> Moved to `credentialsConfig.quotaProject` +- `httpHandler` -> Moved to `transportConfig.rest.httpHandler` +- `authHttpHandler` -> Moved to `credentialsConfig.authHttpHandler` +- `retries` -> Removed from client options and moved to call options `retrySettings.maxRetries` + +### Retry Options changes + +The retry options have been moved to use [`RetrySettings`][RetrySettings] in call options +and function parameters. + +- `retries` -> Renamed to `retrySettings.maxRetries` +- `maxRetries` -> Renamed to `retrySettings.maxRetries` + +[RetrySettings]: https://googleapis.github.io/gax-php/v1.26.1/Google/ApiCore/RetrySettings.html + +[ClientOptions]: https://googleapis.github.io/gax-php/v1.26.1/Google/ApiCore/Options/ClientOptions.html + +### Connection classes are not used anymore. + +This is a major change with this major version but one that we hope won't break any users. When the +`SpannerClient` was created, behind the scenes a connection adapter was initialized. +This connection object was then forwarded to any resource classes internally, +like so: + +```php +// This initialized a connection object +$client = new SpannerClient(); +// This passed on the connection object to the Instance class +$instance = $spanner->instance('my-instance'); +``` + +As you can see the connection object was handled internally. If you used the library in this way, +you will not need to make any changes. However, if you created the connection classes directly +and passed it to the `Instance` class, this will break in Spanner `v2`: + +```php +// Not intended +$connObj = new Grpc([]); +$instance = new Instance( + $connObj, + // other constructor options +); +``` + +### `Google\Cloud\Spanner\Duration` class is not used anymore. +We have removed the `Google\Cloud\Spanner\Duration` class from the library. Instead we will be using the `Google\Protobuf\Duration` class. + +### IAM class changes + +We have kept the functionality of `IAM` the same, however the underlying `IAM` class has changed. +```php +// In V1, this used to return an instance of Google\Cloud\Core\Iam\Iam +$iam = $instance->iam(); + +// In V2, this will return an instance of Google\Cloud\Core\Iam\IamManager +$iam = $instance->iam(); + +// Both the classes share the same functionality, so the following methods will work for both versions. +$iam->policy(); +$iam->setPolicy(); +$iam->testIamPermissions(); +``` + +### LongRunningOperation class changes + +We have kept the functionality of `LongRunningOperation` the same, +however the underlying `LongRunningOperation` class has changed. +```php +// In V1, this used to return an instance of Google\Cloud\Core\LongRunning\LongRunningOperation. +$lro = $instance->create($configuration); + +// In V2, this will return an instance of Google\ApiCore\OperationResponse. +$lro = $instance->create($configuration); + +// Both the classes share the same functionality, so the following methods will work for both versions. +$lro->name(); +$lro->done(); +$lro->state(); +$lro->result(); +$lro->error(); +$lro->info(); +$lro->reload(); +$lro->pollUntilComplete(); +$lro->cancel(); +$lro->delete(); +``` + +### Removed Methods + + - `Operation::createTransaction` => use `Operation::transaction` instead + - `Operation::createSnapshot` => use `Operation::snapshot` instead \ No newline at end of file diff --git a/Spanner/composer.json b/Spanner/composer.json index e3b84653fbbd..8d2fdf36d700 100644 --- a/Spanner/composer.json +++ b/Spanner/composer.json @@ -6,7 +6,7 @@ "require": { "php": "^8.1", "ext-grpc": "*", - "google/cloud-core": "^1.57", + "google/cloud-core": "^1.68", "google/gax": "^1.38.0" }, "require-dev": { @@ -16,7 +16,9 @@ "phpdocumentor/reflection": "^5.3.3||^6.0", "phpdocumentor/reflection-docblock": "^5.3", "erusev/parsedown": "^1.6", - "google/cloud-pubsub": "^2.0" + "google/cloud-pubsub": "^2.0", + "dg/bypass-finals": "^1.7", + "dms/phpunit-arraysubset-asserts": "^0.5.0" }, "suggest": { "ext-protobuf": "Provides a significant increase in throughput over the pure PHP protobuf implementation. See https://cloud.google.com/php/grpc for installation instructions.", diff --git a/Spanner/phpstan.neon.dist b/Spanner/phpstan.neon.dist new file mode 100644 index 000000000000..7c6abb388f8e --- /dev/null +++ b/Spanner/phpstan.neon.dist @@ -0,0 +1,13 @@ +parameters: + level: 3 + treatPhpDocTypesAsCertain: false + excludePaths: + # Ignore classes not compatible with Monolog2 + - src/V1/* + - src/*/*/V1/* + ignoreErrors: + - + identifier: new.static # Ignore this error because changing it would be a breaking change. +includes: + - ../dev/vendor/phpstan/phpstan/conf/bleedingEdge.neon + diff --git a/Spanner/phpunit.xml.dist b/Spanner/phpunit.xml.dist index 9923818f349f..33fc08b07b98 100644 --- a/Spanner/phpunit.xml.dist +++ b/Spanner/phpunit.xml.dist @@ -1,5 +1,5 @@ - + src diff --git a/Spanner/src/Admin/Database/V1/DatabaseAdminClient.php b/Spanner/src/Admin/Database/V1/DatabaseAdminClient.php deleted file mode 100644 index 531fff1ad3c3..000000000000 --- a/Spanner/src/Admin/Database/V1/DatabaseAdminClient.php +++ /dev/null @@ -1,42 +0,0 @@ -databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * $splitPoints = []; - * $response = $databaseAdminClient->addSplitPoints($formattedDatabase, $splitPoints); - * } 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 parseName method to extract the individual identifiers - * contained within formatted names that are returned by the API. - * - * @deprecated Please use the new service client {@see \Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient}. - */ -class DatabaseAdminGapicClient -{ - use GapicClientTrait; - - /** The name of the service. */ - const SERVICE_NAME = 'google.spanner.admin.database.v1.DatabaseAdmin'; - - /** - * The default address of the service. - * - * @deprecated SERVICE_ADDRESS_TEMPLATE should be used instead. - */ - const SERVICE_ADDRESS = 'spanner.googleapis.com'; - - /** The address template of the service. */ - private const SERVICE_ADDRESS_TEMPLATE = 'spanner.UNIVERSE_DOMAIN'; - - /** The default port of the service. */ - const DEFAULT_SERVICE_PORT = 443; - - /** The name of the code generator, to be included in the agent header. */ - const CODEGEN_NAME = 'gapic'; - - /** The default scopes required by the service. */ - public static $serviceScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.admin', - ]; - - private static $backupNameTemplate; - - private static $backupScheduleNameTemplate; - - private static $cryptoKeyNameTemplate; - - private static $cryptoKeyVersionNameTemplate; - - private static $databaseNameTemplate; - - private static $instanceNameTemplate; - - private static $instancePartitionNameTemplate; - - private static $pathTemplateMap; - - private $operationsClient; - - private static function getClientDefaults() - { - return [ - 'serviceName' => self::SERVICE_NAME, - 'apiEndpoint' => - self::SERVICE_ADDRESS . ':' . self::DEFAULT_SERVICE_PORT, - 'clientConfig' => - __DIR__ . '/../resources/database_admin_client_config.json', - 'descriptorsConfigPath' => - __DIR__ . '/../resources/database_admin_descriptor_config.php', - 'gcpApiConfigPath' => - __DIR__ . '/../resources/database_admin_grpc_config.json', - 'credentialsConfig' => [ - 'defaultScopes' => self::$serviceScopes, - ], - 'transportConfig' => [ - 'rest' => [ - 'restClientConfigPath' => - __DIR__ . - '/../resources/database_admin_rest_client_config.php', - ], - ], - ]; - } - - private static function getBackupNameTemplate() - { - if (self::$backupNameTemplate == null) { - self::$backupNameTemplate = new PathTemplate( - 'projects/{project}/instances/{instance}/backups/{backup}' - ); - } - - return self::$backupNameTemplate; - } - - private static function getBackupScheduleNameTemplate() - { - if (self::$backupScheduleNameTemplate == null) { - self::$backupScheduleNameTemplate = new PathTemplate( - 'projects/{project}/instances/{instance}/databases/{database}/backupSchedules/{schedule}' - ); - } - - return self::$backupScheduleNameTemplate; - } - - private static function getCryptoKeyNameTemplate() - { - if (self::$cryptoKeyNameTemplate == null) { - self::$cryptoKeyNameTemplate = new PathTemplate( - 'projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key}' - ); - } - - return self::$cryptoKeyNameTemplate; - } - - private static function getCryptoKeyVersionNameTemplate() - { - if (self::$cryptoKeyVersionNameTemplate == null) { - self::$cryptoKeyVersionNameTemplate = new PathTemplate( - 'projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key}/cryptoKeyVersions/{crypto_key_version}' - ); - } - - return self::$cryptoKeyVersionNameTemplate; - } - - private static function getDatabaseNameTemplate() - { - if (self::$databaseNameTemplate == null) { - self::$databaseNameTemplate = new PathTemplate( - 'projects/{project}/instances/{instance}/databases/{database}' - ); - } - - return self::$databaseNameTemplate; - } - - private static function getInstanceNameTemplate() - { - if (self::$instanceNameTemplate == null) { - self::$instanceNameTemplate = new PathTemplate( - 'projects/{project}/instances/{instance}' - ); - } - - return self::$instanceNameTemplate; - } - - private static function getInstancePartitionNameTemplate() - { - if (self::$instancePartitionNameTemplate == null) { - self::$instancePartitionNameTemplate = new PathTemplate( - 'projects/{project}/instances/{instance}/instancePartitions/{instance_partition}' - ); - } - - return self::$instancePartitionNameTemplate; - } - - private static function getPathTemplateMap() - { - if (self::$pathTemplateMap == null) { - self::$pathTemplateMap = [ - 'backup' => self::getBackupNameTemplate(), - 'backupSchedule' => self::getBackupScheduleNameTemplate(), - 'cryptoKey' => self::getCryptoKeyNameTemplate(), - 'cryptoKeyVersion' => self::getCryptoKeyVersionNameTemplate(), - 'database' => self::getDatabaseNameTemplate(), - 'instance' => self::getInstanceNameTemplate(), - 'instancePartition' => self::getInstancePartitionNameTemplate(), - ]; - } - - return self::$pathTemplateMap; - } - - /** - * Formats a string containing the fully-qualified path to represent a backup - * resource. - * - * @param string $project - * @param string $instance - * @param string $backup - * - * @return string The formatted backup resource. - */ - public static function backupName($project, $instance, $backup) - { - return self::getBackupNameTemplate()->render([ - 'project' => $project, - 'instance' => $instance, - 'backup' => $backup, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a - * backup_schedule resource. - * - * @param string $project - * @param string $instance - * @param string $database - * @param string $schedule - * - * @return string The formatted backup_schedule resource. - */ - public static function backupScheduleName( - $project, - $instance, - $database, - $schedule - ) { - return self::getBackupScheduleNameTemplate()->render([ - 'project' => $project, - 'instance' => $instance, - 'database' => $database, - 'schedule' => $schedule, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a crypto_key - * resource. - * - * @param string $project - * @param string $location - * @param string $keyRing - * @param string $cryptoKey - * - * @return string The formatted crypto_key resource. - */ - public static function cryptoKeyName( - $project, - $location, - $keyRing, - $cryptoKey - ) { - return self::getCryptoKeyNameTemplate()->render([ - 'project' => $project, - 'location' => $location, - 'key_ring' => $keyRing, - 'crypto_key' => $cryptoKey, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a - * crypto_key_version resource. - * - * @param string $project - * @param string $location - * @param string $keyRing - * @param string $cryptoKey - * @param string $cryptoKeyVersion - * - * @return string The formatted crypto_key_version resource. - */ - public static function cryptoKeyVersionName( - $project, - $location, - $keyRing, - $cryptoKey, - $cryptoKeyVersion - ) { - return self::getCryptoKeyVersionNameTemplate()->render([ - 'project' => $project, - 'location' => $location, - 'key_ring' => $keyRing, - 'crypto_key' => $cryptoKey, - 'crypto_key_version' => $cryptoKeyVersion, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a database - * resource. - * - * @param string $project - * @param string $instance - * @param string $database - * - * @return string The formatted database resource. - */ - public static function databaseName($project, $instance, $database) - { - return self::getDatabaseNameTemplate()->render([ - 'project' => $project, - 'instance' => $instance, - 'database' => $database, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a instance - * resource. - * - * @param string $project - * @param string $instance - * - * @return string The formatted instance resource. - */ - public static function instanceName($project, $instance) - { - return self::getInstanceNameTemplate()->render([ - 'project' => $project, - 'instance' => $instance, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a - * instance_partition resource. - * - * @param string $project - * @param string $instance - * @param string $instancePartition - * - * @return string The formatted instance_partition resource. - */ - public static function instancePartitionName( - $project, - $instance, - $instancePartition - ) { - return self::getInstancePartitionNameTemplate()->render([ - 'project' => $project, - 'instance' => $instance, - 'instance_partition' => $instancePartition, - ]); - } - - /** - * Parses a formatted name string and returns an associative array of the components in the name. - * The following name formats are supported: - * Template: Pattern - * - backup: projects/{project}/instances/{instance}/backups/{backup} - * - backupSchedule: projects/{project}/instances/{instance}/databases/{database}/backupSchedules/{schedule} - * - cryptoKey: projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key} - * - cryptoKeyVersion: projects/{project}/locations/{location}/keyRings/{key_ring}/cryptoKeys/{crypto_key}/cryptoKeyVersions/{crypto_key_version} - * - database: projects/{project}/instances/{instance}/databases/{database} - * - instance: projects/{project}/instances/{instance} - * - instancePartition: projects/{project}/instances/{instance}/instancePartitions/{instance_partition} - * - * The optional $template argument can be supplied to specify a particular pattern, - * and must match one of the templates listed above. If no $template argument is - * provided, or if the $template argument does not match one of the templates - * listed, then parseName will check each of the supported templates, and return - * the first match. - * - * @param string $formattedName The formatted name string - * @param string $template Optional name of template to match - * - * @return array An associative array from name component IDs to component values. - * - * @throws ValidationException If $formattedName could not be matched. - */ - public static function parseName($formattedName, $template = null) - { - $templateMap = self::getPathTemplateMap(); - if ($template) { - if (!isset($templateMap[$template])) { - throw new ValidationException( - "Template name $template does not exist" - ); - } - - return $templateMap[$template]->match($formattedName); - } - - foreach ($templateMap as $templateName => $pathTemplate) { - try { - return $pathTemplate->match($formattedName); - } catch (ValidationException $ex) { - // Swallow the exception to continue trying other path templates - } - } - - throw new ValidationException( - "Input did not match any known format. Input: $formattedName" - ); - } - - /** - * Return an OperationsClient object with the same endpoint as $this. - * - * @return 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 OperationResponse - */ - public function resumeOperation($operationName, $methodName = null) - { - $options = isset($this->descriptors[$methodName]['longRunning']) - ? $this->descriptors[$methodName]['longRunning'] - : []; - $operation = new OperationResponse( - $operationName, - $this->getOperationsClient(), - $options - ); - $operation->reload(); - return $operation; - } - - /** - * Constructor. - * - * @param array $options { - * Optional. Options for configuring the service API wrapper. - * - * @type string $apiEndpoint - * The address of the API remote host. May optionally include the port, formatted - * as ":". Default 'spanner.googleapis.com:443'. - * @type string|array|FetchAuthTokenInterface|CredentialsWrapper $credentials - * The credentials to be used by the client to authorize API calls. This option - * accepts either a path to a credentials file, or a decoded credentials file as a - * PHP array. - * *Advanced usage*: In addition, this option can also accept a pre-constructed - * {@see \Google\Auth\FetchAuthTokenInterface} object or - * {@see \Google\ApiCore\CredentialsWrapper} object. Note that when one of these - * objects are provided, any settings in $credentialsConfig will be ignored. - * @type array $credentialsConfig - * Options used to configure credentials, including auth token caching, for the - * client. For a full list of supporting configuration options, see - * {@see \Google\ApiCore\CredentialsWrapper::build()} . - * @type bool $disableRetries - * Determines whether or not retries defined by the client configuration should be - * disabled. Defaults to `false`. - * @type string|array $clientConfig - * Client method configuration, including retry settings. This option can be either - * a path to a JSON file, or a PHP array containing the decoded JSON data. By - * default this settings points to the default client config file, which is - * provided in the resources folder. - * @type string|TransportInterface $transport - * The transport used for executing network requests. May be either the string - * `rest` or `grpc`. Defaults to `grpc` if gRPC support is detected on the system. - * *Advanced usage*: Additionally, it is possible to pass in an already - * instantiated {@see \Google\ApiCore\Transport\TransportInterface} object. Note - * that when this object is provided, any settings in $transportConfig, and any - * $apiEndpoint setting, will be ignored. - * @type array $transportConfig - * Configuration options that will be used to construct the transport. Options for - * each supported transport type should be passed in a key for that transport. For - * example: - * $transportConfig = [ - * 'grpc' => [...], - * 'rest' => [...], - * ]; - * See the {@see \Google\ApiCore\Transport\GrpcTransport::build()} and - * {@see \Google\ApiCore\Transport\RestTransport::build()} methods for the - * supported options. - * @type callable $clientCertSource - * A callable which returns the client cert as a string. This can be used to - * provide a certificate and private key to the transport layer for mTLS. - * } - * - * @throws ValidationException - */ - public function __construct(array $options = []) - { - $clientOptions = $this->buildClientOptions($options); - $this->setClientOptions($clientOptions); - $this->operationsClient = $this->createOperationsClient($clientOptions); - } - - /** - * Adds split points to specified tables, indexes of a database. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedDatabase = $databaseAdminClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * $splitPoints = []; - * $response = $databaseAdminClient->addSplitPoints($formattedDatabase, $splitPoints); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $database Required. The database on whose tables/indexes split points are to be - * added. Values are of the form - * `projects//instances//databases/`. - * @param SplitPoints[] $splitPoints Required. The split points to add. - * @param array $optionalArgs { - * Optional. - * - * @type string $initiator - * Optional. A user-supplied tag associated with the split points. - * For example, "intital_data_load", "special_event_1". - * Defaults to "CloudAddSplitPointsAPI" if not specified. - * The length of the tag must not exceed 50 characters,else will be trimmed. - * Only valid UTF8 characters are allowed. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\AddSplitPointsResponse - * - * @throws ApiException if the remote call fails - */ - public function addSplitPoints( - $database, - $splitPoints, - array $optionalArgs = [] - ) { - $request = new AddSplitPointsRequest(); - $requestParamHeaders = []; - $request->setDatabase($database); - $request->setSplitPoints($splitPoints); - $requestParamHeaders['database'] = $database; - if (isset($optionalArgs['initiator'])) { - $request->setInitiator($optionalArgs['initiator']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'AddSplitPoints', - AddSplitPointsResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Starts copying a Cloud Spanner Backup. - * The returned backup [long-running operation][google.longrunning.Operation] - * will have a name of the format - * `projects//instances//backups//operations/` - * and can be used to track copying of the backup. The operation is associated - * with the destination backup. - * The [metadata][google.longrunning.Operation.metadata] field type is - * [CopyBackupMetadata][google.spanner.admin.database.v1.CopyBackupMetadata]. - * The [response][google.longrunning.Operation.response] field type is - * [Backup][google.spanner.admin.database.v1.Backup], if successful. - * Cancelling the returned operation will stop the copying and delete the - * destination backup. Concurrent CopyBackup requests can run on the same - * source backup. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * $backupId = 'backup_id'; - * $formattedSourceBackup = $databaseAdminClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - * $expireTime = new Timestamp(); - * $operationResponse = $databaseAdminClient->copyBackup($formattedParent, $backupId, $formattedSourceBackup, $expireTime); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $databaseAdminClient->copyBackup($formattedParent, $backupId, $formattedSourceBackup, $expireTime); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'copyBackup'); - * 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 destination instance that will contain the backup - * copy. Values are of the form: `projects//instances/`. - * @param string $backupId Required. The id of the backup copy. - * The `backup_id` appended to `parent` forms the full backup_uri of the form - * `projects//instances//backups/`. - * @param string $sourceBackup Required. The source backup to be copied. - * The source backup needs to be in READY state for it to be copied. - * Once CopyBackup is in progress, the source backup cannot be deleted or - * cleaned up on expiration until CopyBackup is finished. - * Values are of the form: - * `projects//instances//backups/`. - * @param Timestamp $expireTime Required. The expiration time of the backup in microsecond granularity. - * The expiration time must be at least 6 hours and at most 366 days - * from the `create_time` of the source backup. Once the `expire_time` has - * passed, the backup is eligible to be automatically deleted by Cloud Spanner - * to free the resources used by the backup. - * @param array $optionalArgs { - * Optional. - * - * @type CopyBackupEncryptionConfig $encryptionConfig - * Optional. The encryption configuration used to encrypt the backup. If this - * field is not specified, the backup will use the same encryption - * configuration as the source backup by default, namely - * [encryption_type][google.spanner.admin.database.v1.CopyBackupEncryptionConfig.encryption_type] - * = `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function copyBackup( - $parent, - $backupId, - $sourceBackup, - $expireTime, - array $optionalArgs = [] - ) { - $request = new CopyBackupRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $request->setBackupId($backupId); - $request->setSourceBackup($sourceBackup); - $request->setExpireTime($expireTime); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['encryptionConfig'])) { - $request->setEncryptionConfig($optionalArgs['encryptionConfig']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'CopyBackup', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * Starts creating a new Cloud Spanner Backup. - * The returned backup [long-running operation][google.longrunning.Operation] - * will have a name of the format - * `projects//instances//backups//operations/` - * and can be used to track creation of the backup. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. - * The [response][google.longrunning.Operation.response] field type is - * [Backup][google.spanner.admin.database.v1.Backup], if successful. - * Cancelling the returned operation will stop the creation and delete the - * backup. There can be only one pending backup creation per database. Backup - * creation of different databases can run concurrently. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * $backupId = 'backup_id'; - * $backup = new Backup(); - * $operationResponse = $databaseAdminClient->createBackup($formattedParent, $backupId, $backup); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $databaseAdminClient->createBackup($formattedParent, $backupId, $backup); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'createBackup'); - * 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 in which the backup will be - * created. This must be the same instance that contains the database the - * backup will be created from. The backup will be stored in the - * location(s) specified in the instance configuration of this - * instance. Values are of the form - * `projects//instances/`. - * @param string $backupId Required. The id of the backup to be created. The `backup_id` appended to - * `parent` forms the full backup name of the form - * `projects//instances//backups/`. - * @param Backup $backup Required. The backup to create. - * @param array $optionalArgs { - * Optional. - * - * @type CreateBackupEncryptionConfig $encryptionConfig - * Optional. The encryption configuration used to encrypt the backup. If this - * field is not specified, the backup will use the same encryption - * configuration as the database by default, namely - * [encryption_type][google.spanner.admin.database.v1.CreateBackupEncryptionConfig.encryption_type] - * = `USE_DATABASE_ENCRYPTION`. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function createBackup( - $parent, - $backupId, - $backup, - array $optionalArgs = [] - ) { - $request = new CreateBackupRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $request->setBackupId($backupId); - $request->setBackup($backup); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['encryptionConfig'])) { - $request->setEncryptionConfig($optionalArgs['encryptionConfig']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'CreateBackup', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * Creates a new backup schedule. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * $backupScheduleId = 'backup_schedule_id'; - * $backupSchedule = new BackupSchedule(); - * $response = $databaseAdminClient->createBackupSchedule($formattedParent, $backupScheduleId, $backupSchedule); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. The name of the database that this backup schedule applies to. - * @param string $backupScheduleId Required. The Id to use for the backup schedule. The `backup_schedule_id` - * appended to `parent` forms the full backup schedule name of the form - * `projects//instances//databases//backupSchedules/`. - * @param BackupSchedule $backupSchedule Required. The backup schedule to create. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\BackupSchedule - * - * @throws ApiException if the remote call fails - */ - public function createBackupSchedule( - $parent, - $backupScheduleId, - $backupSchedule, - array $optionalArgs = [] - ) { - $request = new CreateBackupScheduleRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $request->setBackupScheduleId($backupScheduleId); - $request->setBackupSchedule($backupSchedule); - $requestParamHeaders['parent'] = $parent; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'CreateBackupSchedule', - BackupSchedule::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * $createStatement = 'create_statement'; - * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // 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. - * If the database ID is a reserved word or if it contains a hyphen, the - * database ID must be enclosed in backticks (`` ` ``). - * @param array $optionalArgs { - * Optional. - * - * @type string[] $extraStatements - * Optional. A 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 EncryptionConfig $encryptionConfig - * Optional. The encryption configuration for the database. If this field is - * not specified, Cloud Spanner will encrypt/decrypt all data at rest using - * Google default encryption. - * @type int $databaseDialect - * Optional. The dialect of the Cloud Spanner Database. - * For allowed values, use constants defined on {@see \Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect} - * @type string $protoDescriptors - * Optional. Proto descriptors used by CREATE/ALTER PROTO BUNDLE statements in - * 'extra_statements' above. - * Contains a protobuf-serialized - * [google.protobuf.FileDescriptorSet](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto). - * To generate it, [install](https://grpc.io/docs/protoc-installation/) and - * run `protoc` with --include_imports and --descriptor_set_out. For example, - * to generate for moon/shot/app.proto, run - * ``` - * $protoc --proto_path=/app_path --proto_path=/lib_path \ - * --include_imports \ - * --descriptor_set_out=descriptors.data \ - * moon/shot/app.proto - * ``` - * For more details, see protobuffer [self - * description](https://developers.google.com/protocol-buffers/docs/techniques#self-description). - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function createDatabase( - $parent, - $createStatement, - array $optionalArgs = [] - ) { - $request = new CreateDatabaseRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $request->setCreateStatement($createStatement); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['extraStatements'])) { - $request->setExtraStatements($optionalArgs['extraStatements']); - } - - if (isset($optionalArgs['encryptionConfig'])) { - $request->setEncryptionConfig($optionalArgs['encryptionConfig']); - } - - if (isset($optionalArgs['databaseDialect'])) { - $request->setDatabaseDialect($optionalArgs['databaseDialect']); - } - - if (isset($optionalArgs['protoDescriptors'])) { - $request->setProtoDescriptors($optionalArgs['protoDescriptors']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'CreateDatabase', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * Deletes a pending or completed - * [Backup][google.spanner.admin.database.v1.Backup]. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedName = $databaseAdminClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - * $databaseAdminClient->deleteBackup($formattedName); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $name Required. Name of the backup to delete. - * Values are of the form - * `projects//instances//backups/`. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @throws ApiException if the remote call fails - */ - public function deleteBackup($name, array $optionalArgs = []) - { - $request = new DeleteBackupRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'DeleteBackup', - GPBEmpty::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Deletes a backup schedule. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedName = $databaseAdminClient->backupScheduleName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SCHEDULE]'); - * $databaseAdminClient->deleteBackupSchedule($formattedName); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $name Required. The name of the schedule to delete. - * Values are of the form - * `projects//instances//databases//backupSchedules/`. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @throws ApiException if the remote call fails - */ - public function deleteBackupSchedule($name, array $optionalArgs = []) - { - $request = new DeleteBackupScheduleRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'DeleteBackupSchedule', - GPBEmpty::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Drops (aka deletes) a Cloud Spanner database. - * Completed backups for the database will be retained according to their - * `expire_time`. - * Note: Cloud Spanner might continue to accept requests for a few seconds - * after the database has been deleted. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedDatabase = $databaseAdminClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * $databaseAdminClient->dropDatabase($formattedDatabase); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $database Required. The database to be dropped. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @throws ApiException if the remote call fails - */ - public function dropDatabase($database, array $optionalArgs = []) - { - $request = new DropDatabaseRequest(); - $requestParamHeaders = []; - $request->setDatabase($database); - $requestParamHeaders['database'] = $database; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'DropDatabase', - GPBEmpty::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Gets metadata on a pending or completed - * [Backup][google.spanner.admin.database.v1.Backup]. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedName = $databaseAdminClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - * $response = $databaseAdminClient->getBackup($formattedName); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $name Required. Name of the backup. - * Values are of the form - * `projects//instances//backups/`. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\Backup - * - * @throws ApiException if the remote call fails - */ - public function getBackup($name, array $optionalArgs = []) - { - $request = new GetBackupRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetBackup', - Backup::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Gets backup schedule for the input schedule name. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedName = $databaseAdminClient->backupScheduleName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SCHEDULE]'); - * $response = $databaseAdminClient->getBackupSchedule($formattedName); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $name Required. The name of the schedule to retrieve. - * Values are of the form - * `projects//instances//databases//backupSchedules/`. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\BackupSchedule - * - * @throws ApiException if the remote call fails - */ - public function getBackupSchedule($name, array $optionalArgs = []) - { - $request = new GetBackupScheduleRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetBackupSchedule', - BackupSchedule::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Gets the state of a Cloud Spanner database. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedName = $databaseAdminClient->databaseName('[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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\Database - * - * @throws ApiException if the remote call fails - */ - public function getDatabase($name, array $optionalArgs = []) - { - $request = new GetDatabaseRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetDatabase', - Database::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedDatabase = $databaseAdminClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * $response = $databaseAdminClient->getDatabaseDdl($formattedDatabase); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $database Required. The database whose schema we wish to get. - * Values are of the form - * `projects//instances//databases/` - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseDdlResponse - * - * @throws ApiException if the remote call fails - */ - public function getDatabaseDdl($database, array $optionalArgs = []) - { - $request = new GetDatabaseDdlRequest(); - $requestParamHeaders = []; - $request->setDatabase($database); - $requestParamHeaders['database'] = $database; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetDatabaseDdl', - GetDatabaseDdlResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Gets the access control policy for a database or backup resource. - * Returns an empty policy if a database or backup exists but does not have a - * policy set. - * - * Authorization requires `spanner.databases.getIamPolicy` permission on - * [resource][google.iam.v1.GetIamPolicyRequest.resource]. - * For backups, authorization requires `spanner.backups.getIamPolicy` - * permission on [resource][google.iam.v1.GetIamPolicyRequest.resource]. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $resource = 'resource'; - * $response = $databaseAdminClient->getIamPolicy($resource); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $resource REQUIRED: The resource for which the policy is being requested. - * See the operation documentation for the appropriate value for this field. - * @param array $optionalArgs { - * Optional. - * - * @type GetPolicyOptions $options - * OPTIONAL: A `GetPolicyOptions` object for specifying options to - * `GetIamPolicy`. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Iam\V1\Policy - * - * @throws ApiException if the remote call fails - */ - public function getIamPolicy($resource, array $optionalArgs = []) - { - $request = new GetIamPolicyRequest(); - $requestParamHeaders = []; - $request->setResource($resource); - $requestParamHeaders['resource'] = $resource; - if (isset($optionalArgs['options'])) { - $request->setOptions($optionalArgs['options']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetIamPolicy', - Policy::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * This is an internal API called by Spanner Graph jobs. You should never need - * to call this API directly. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedDatabase = $databaseAdminClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * $operationId = 'operation_id'; - * $vmIdentityToken = 'vm_identity_token'; - * $response = $databaseAdminClient->internalUpdateGraphOperation($formattedDatabase, $operationId, $vmIdentityToken); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $database Internal field, do not use directly. - * @param string $operationId Internal field, do not use directly. - * @param string $vmIdentityToken Internal field, do not use directly. - * @param array $optionalArgs { - * Optional. - * - * @type float $progress - * Internal field, do not use directly. - * @type Status $status - * Internal field, do not use directly. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\InternalUpdateGraphOperationResponse - * - * @throws ApiException if the remote call fails - */ - public function internalUpdateGraphOperation( - $database, - $operationId, - $vmIdentityToken, - array $optionalArgs = [] - ) { - $request = new InternalUpdateGraphOperationRequest(); - $request->setDatabase($database); - $request->setOperationId($operationId); - $request->setVmIdentityToken($vmIdentityToken); - if (isset($optionalArgs['progress'])) { - $request->setProgress($optionalArgs['progress']); - } - - if (isset($optionalArgs['status'])) { - $request->setStatus($optionalArgs['status']); - } - - return $this->startCall( - 'InternalUpdateGraphOperation', - InternalUpdateGraphOperationResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Lists the backup [long-running operations][google.longrunning.Operation] in - * the given instance. A backup operation has a name of the form - * `projects//instances//backups//operations/`. - * The long-running operation - * [metadata][google.longrunning.Operation.metadata] field type - * `metadata.type_url` describes the type of the metadata. Operations returned - * include those that have completed/failed/canceled within the last 7 days, - * and pending operations. Operations returned are ordered by - * `operation.metadata.value.progress.start_time` in descending order starting - * from the most recently started operation. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * // Iterate over pages of elements - * $pagedResponse = $databaseAdminClient->listBackupOperations($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $databaseAdminClient->listBackupOperations($formattedParent); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. The instance of the backup operations. Values are of - * the form `projects//instances/`. - * @param array $optionalArgs { - * Optional. - * - * @type string $filter - * An expression that filters the list of returned backup operations. - * - * A filter expression consists of a field name, a - * comparison operator, and a value for filtering. - * The value must be a string, a number, or a boolean. The comparison operator - * must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. - * Colon `:` is the contains operator. Filter rules are not case sensitive. - * - * The following fields in the [operation][google.longrunning.Operation] - * are eligible for filtering: - * - * * `name` - The name of the long-running operation - * * `done` - False if the operation is in progress, else true. - * * `metadata.@type` - the type of metadata. For example, the type string - * for - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata] - * is - * `type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata`. - * * `metadata.` - any field in metadata.value. - * `metadata.@type` must be specified first if filtering on metadata - * fields. - * * `error` - Error associated with the long-running operation. - * * `response.@type` - the type of response. - * * `response.` - any field in response.value. - * - * You can combine multiple expressions by enclosing each expression in - * parentheses. By default, expressions are combined with AND logic, but - * you can specify AND, OR, and NOT logic explicitly. - * - * Here are a few examples: - * - * * `done:true` - The operation is complete. - * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ - * `metadata.database:prod` - Returns operations where: - * * The operation's metadata type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. - * * The source database name of backup contains the string "prod". - * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ - * `(metadata.name:howl) AND` \ - * `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ - * `(error:*)` - Returns operations where: - * * The operation's metadata type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata]. - * * The backup name contains the string "howl". - * * The operation started before 2018-03-28T14:50:00Z. - * * The operation resulted in an error. - * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CopyBackupMetadata) AND` \ - * `(metadata.source_backup:test) AND` \ - * `(metadata.progress.start_time < \"2022-01-18T14:50:00Z\") AND` \ - * `(error:*)` - Returns operations where: - * * The operation's metadata type is - * [CopyBackupMetadata][google.spanner.admin.database.v1.CopyBackupMetadata]. - * * The source backup name contains the string "test". - * * The operation started before 2022-01-18T14:50:00Z. - * * The operation resulted in an error. - * * `((metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata) AND` \ - * `(metadata.database:test_db)) OR` \ - * `((metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.CopyBackupMetadata) - * AND` \ - * `(metadata.source_backup:test_bkp)) AND` \ - * `(error:*)` - Returns operations where: - * * The operation's metadata matches either of criteria: - * * The operation's metadata type is - * [CreateBackupMetadata][google.spanner.admin.database.v1.CreateBackupMetadata] - * AND the source database name of the backup contains the string - * "test_db" - * * The operation's metadata type is - * [CopyBackupMetadata][google.spanner.admin.database.v1.CopyBackupMetadata] - * AND the source backup name contains the string "test_bkp" - * * The operation resulted in an error. - * @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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listBackupOperations($parent, array $optionalArgs = []) - { - $request = new ListBackupOperationsRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['filter'])) { - $request->setFilter($optionalArgs['filter']); - } - - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListBackupOperations', - $optionalArgs, - ListBackupOperationsResponse::class, - $request - ); - } - - /** - * Lists all the backup schedules for the database. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * // Iterate over pages of elements - * $pagedResponse = $databaseAdminClient->listBackupSchedules($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $databaseAdminClient->listBackupSchedules($formattedParent); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. Database is the parent resource whose backup schedules should be - * listed. Values are of the form - * projects//instances//databases/ - * @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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listBackupSchedules($parent, array $optionalArgs = []) - { - $request = new ListBackupSchedulesRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListBackupSchedules', - $optionalArgs, - ListBackupSchedulesResponse::class, - $request - ); - } - - /** - * Lists completed and pending backups. - * Backups returned are ordered by `create_time` in descending order, - * starting from the most recent `create_time`. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * // Iterate over pages of elements - * $pagedResponse = $databaseAdminClient->listBackups($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $databaseAdminClient->listBackups($formattedParent); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. The instance to list backups from. Values are of the - * form `projects//instances/`. - * @param array $optionalArgs { - * Optional. - * - * @type string $filter - * An expression that filters the list of returned backups. - * - * A filter expression consists of a field name, a comparison operator, and a - * value for filtering. - * The value must be a string, a number, or a boolean. The comparison operator - * must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. - * Colon `:` is the contains operator. Filter rules are not case sensitive. - * - * The following fields in the - * [Backup][google.spanner.admin.database.v1.Backup] are eligible for - * filtering: - * - * * `name` - * * `database` - * * `state` - * * `create_time` (and values are of the format YYYY-MM-DDTHH:MM:SSZ) - * * `expire_time` (and values are of the format YYYY-MM-DDTHH:MM:SSZ) - * * `version_time` (and values are of the format YYYY-MM-DDTHH:MM:SSZ) - * * `size_bytes` - * * `backup_schedules` - * - * You can combine multiple expressions by enclosing each expression in - * parentheses. By default, expressions are combined with AND logic, but - * you can specify AND, OR, and NOT logic explicitly. - * - * Here are a few examples: - * - * * `name:Howl` - The backup's name contains the string "howl". - * * `database:prod` - * - The database's name contains the string "prod". - * * `state:CREATING` - The backup is pending creation. - * * `state:READY` - The backup is fully created and ready for use. - * * `(name:howl) AND (create_time < \"2018-03-28T14:50:00Z\")` - * - The backup name contains the string "howl" and `create_time` - * of the backup is before 2018-03-28T14:50:00Z. - * * `expire_time < \"2018-03-28T14:50:00Z\"` - * - The backup `expire_time` is before 2018-03-28T14:50:00Z. - * * `size_bytes > 10000000000` - The backup's size is greater than 10GB - * * `backup_schedules:daily` - * - The backup is created from a schedule with "daily" in its name. - * @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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listBackups($parent, array $optionalArgs = []) - { - $request = new ListBackupsRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['filter'])) { - $request->setFilter($optionalArgs['filter']); - } - - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListBackups', - $optionalArgs, - ListBackupsResponse::class, - $request - ); - } - - /** - * Lists database [longrunning-operations][google.longrunning.Operation]. - * A database operation has a name of the form - * `projects//instances//databases//operations/`. - * The long-running operation - * [metadata][google.longrunning.Operation.metadata] field type - * `metadata.type_url` describes the type of the metadata. Operations returned - * include those that have completed/failed/canceled within the last 7 days, - * and pending operations. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * // Iterate over pages of elements - * $pagedResponse = $databaseAdminClient->listDatabaseOperations($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $databaseAdminClient->listDatabaseOperations($formattedParent); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. The instance of the database operations. - * Values are of the form `projects//instances/`. - * @param array $optionalArgs { - * Optional. - * - * @type string $filter - * An expression that filters the list of returned operations. - * - * A filter expression consists of a field name, a - * comparison operator, and a value for filtering. - * The value must be a string, a number, or a boolean. The comparison operator - * must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. - * Colon `:` is the contains operator. Filter rules are not case sensitive. - * - * The following fields in the [Operation][google.longrunning.Operation] - * are eligible for filtering: - * - * * `name` - The name of the long-running operation - * * `done` - False if the operation is in progress, else true. - * * `metadata.@type` - the type of metadata. For example, the type string - * for - * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata] - * is - * `type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata`. - * * `metadata.` - any field in metadata.value. - * `metadata.@type` must be specified first, if filtering on metadata - * fields. - * * `error` - Error associated with the long-running operation. - * * `response.@type` - the type of response. - * * `response.` - any field in response.value. - * - * You can combine multiple expressions by enclosing each expression in - * parentheses. By default, expressions are combined with AND logic. However, - * you can specify AND, OR, and NOT logic explicitly. - * - * Here are a few examples: - * - * * `done:true` - The operation is complete. - * * `(metadata.@type=type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata) AND` \ - * `(metadata.source_type:BACKUP) AND` \ - * `(metadata.backup_info.backup:backup_howl) AND` \ - * `(metadata.name:restored_howl) AND` \ - * `(metadata.progress.start_time < \"2018-03-28T14:50:00Z\") AND` \ - * `(error:*)` - Return operations where: - * * The operation's metadata type is - * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. - * * The database is restored from a backup. - * * The backup name contains "backup_howl". - * * The restored database's name contains "restored_howl". - * * The operation started before 2018-03-28T14:50:00Z. - * * The operation resulted in an error. - * @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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listDatabaseOperations($parent, array $optionalArgs = []) - { - $request = new ListDatabaseOperationsRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['filter'])) { - $request->setFilter($optionalArgs['filter']); - } - - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListDatabaseOperations', - $optionalArgs, - ListDatabaseOperationsResponse::class, - $request - ); - } - - /** - * Lists Cloud Spanner database roles. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * // Iterate over pages of elements - * $pagedResponse = $databaseAdminClient->listDatabaseRoles($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $databaseAdminClient->listDatabaseRoles($formattedParent); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. The database whose roles should be listed. - * Values are of the form - * `projects//instances//databases/`. - * @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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listDatabaseRoles($parent, array $optionalArgs = []) - { - $request = new ListDatabaseRolesRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListDatabaseRoles', - $optionalArgs, - ListDatabaseRolesResponse::class, - $request - ); - } - - /** - * Lists Cloud Spanner databases. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * // Iterate over pages of elements - * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent); - * foreach ($pagedResponse->iterateAllElements() 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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listDatabases($parent, array $optionalArgs = []) - { - $request = new ListDatabasesRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListDatabases', - $optionalArgs, - ListDatabasesResponse::class, - $request - ); - } - - /** - * Create a new database by restoring from a completed backup. The new - * database must be in the same project and in an instance with the same - * instance configuration as the instance containing - * the backup. The returned database [long-running - * operation][google.longrunning.Operation] has a name of the format - * `projects//instances//databases//operations/`, - * and can be used to track the progress of the operation, and to cancel it. - * The [metadata][google.longrunning.Operation.metadata] field type is - * [RestoreDatabaseMetadata][google.spanner.admin.database.v1.RestoreDatabaseMetadata]. - * The [response][google.longrunning.Operation.response] type - * is [Database][google.spanner.admin.database.v1.Database], if - * successful. Cancelling the returned operation will stop the restore and - * delete the database. - * There can be only one database being restored into an instance at a time. - * Once the restore operation completes, a new restore operation can be - * initiated, without waiting for the optimize operation associated with the - * first restore to complete. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedParent = $databaseAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * $databaseId = 'database_id'; - * $operationResponse = $databaseAdminClient->restoreDatabase($formattedParent, $databaseId); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $databaseAdminClient->restoreDatabase($formattedParent, $databaseId); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'restoreDatabase'); - * 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 in which to create the - * restored database. This instance must be in the same project and - * have the same instance configuration as the instance containing - * the source backup. Values are of the form - * `projects//instances/`. - * @param string $databaseId Required. The id of the database to create and restore to. This - * database must not already exist. The `database_id` appended to - * `parent` forms the full database name of the form - * `projects//instances//databases/`. - * @param array $optionalArgs { - * Optional. - * - * @type string $backup - * Name of the backup from which to restore. Values are of the form - * `projects//instances//backups/`. - * @type RestoreDatabaseEncryptionConfig $encryptionConfig - * Optional. An encryption configuration describing the encryption type and - * key resources in Cloud KMS used to encrypt/decrypt the database to restore - * to. If this field is not specified, the restored database will use the same - * encryption configuration as the backup by default, namely - * [encryption_type][google.spanner.admin.database.v1.RestoreDatabaseEncryptionConfig.encryption_type] - * = `USE_CONFIG_DEFAULT_OR_BACKUP_ENCRYPTION`. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function restoreDatabase( - $parent, - $databaseId, - array $optionalArgs = [] - ) { - $request = new RestoreDatabaseRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $request->setDatabaseId($databaseId); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['backup'])) { - $request->setBackup($optionalArgs['backup']); - } - - if (isset($optionalArgs['encryptionConfig'])) { - $request->setEncryptionConfig($optionalArgs['encryptionConfig']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'RestoreDatabase', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * Sets the access control policy on a database or backup resource. - * Replaces any existing policy. - * - * Authorization requires `spanner.databases.setIamPolicy` - * permission on [resource][google.iam.v1.SetIamPolicyRequest.resource]. - * For backups, authorization requires `spanner.backups.setIamPolicy` - * permission on [resource][google.iam.v1.SetIamPolicyRequest.resource]. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $resource = 'resource'; - * $policy = new Policy(); - * $response = $databaseAdminClient->setIamPolicy($resource, $policy); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $resource REQUIRED: The resource for which the policy is being specified. - * See the operation documentation for the appropriate value for this field. - * @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 FieldMask $updateMask - * OPTIONAL: A FieldMask specifying which fields of the policy to modify. Only - * the fields in the mask will be modified. If no mask is provided, the - * following default mask is used: - * - * `paths: "bindings, etag"` - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Iam\V1\Policy - * - * @throws ApiException if the remote call fails - */ - public function setIamPolicy($resource, $policy, array $optionalArgs = []) - { - $request = new SetIamPolicyRequest(); - $requestParamHeaders = []; - $request->setResource($resource); - $request->setPolicy($policy); - $requestParamHeaders['resource'] = $resource; - if (isset($optionalArgs['updateMask'])) { - $request->setUpdateMask($optionalArgs['updateMask']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'SetIamPolicy', - Policy::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Returns permissions that the caller has on the specified database or backup - * 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. - * Calling this method on a backup that does not exist will - * result in a NOT_FOUND error if the user has - * `spanner.backups.list` permission on the containing instance. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $resource = 'resource'; - * $permissions = []; - * $response = $databaseAdminClient->testIamPermissions($resource, $permissions); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param string $resource REQUIRED: The resource for which the policy detail is being requested. - * See the operation documentation for the appropriate value for this field. - * @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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Iam\V1\TestIamPermissionsResponse - * - * @throws ApiException if the remote call fails - */ - public function testIamPermissions( - $resource, - $permissions, - array $optionalArgs = [] - ) { - $request = new TestIamPermissionsRequest(); - $requestParamHeaders = []; - $request->setResource($resource); - $request->setPermissions($permissions); - $requestParamHeaders['resource'] = $resource; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'TestIamPermissions', - TestIamPermissionsResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Updates a pending or completed - * [Backup][google.spanner.admin.database.v1.Backup]. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $backup = new Backup(); - * $updateMask = new FieldMask(); - * $response = $databaseAdminClient->updateBackup($backup, $updateMask); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param Backup $backup Required. The backup to update. `backup.name`, and the fields to be updated - * as specified by `update_mask` are required. Other fields are ignored. - * Update is only supported for the following fields: - * * `backup.expire_time`. - * @param FieldMask $updateMask Required. A mask specifying which fields (e.g. `expire_time`) in the - * Backup resource should be updated. This mask is relative to the Backup - * resource, not to the request message. The field mask must always be - * specified; this prevents any future fields from being erased accidentally - * by clients that do not know about them. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\Backup - * - * @throws ApiException if the remote call fails - */ - public function updateBackup($backup, $updateMask, array $optionalArgs = []) - { - $request = new UpdateBackupRequest(); - $requestParamHeaders = []; - $request->setBackup($backup); - $request->setUpdateMask($updateMask); - $requestParamHeaders['backup.name'] = $backup->getName(); - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'UpdateBackup', - Backup::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Updates a backup schedule. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $backupSchedule = new BackupSchedule(); - * $updateMask = new FieldMask(); - * $response = $databaseAdminClient->updateBackupSchedule($backupSchedule, $updateMask); - * } finally { - * $databaseAdminClient->close(); - * } - * ``` - * - * @param BackupSchedule $backupSchedule Required. The backup schedule to update. `backup_schedule.name`, and the - * fields to be updated as specified by `update_mask` are required. Other - * fields are ignored. - * @param FieldMask $updateMask Required. A mask specifying which fields in the BackupSchedule resource - * should be updated. This mask is relative to the BackupSchedule resource, - * not to the request message. The field mask must always be - * specified; this prevents any future fields from being erased - * accidentally. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Database\V1\BackupSchedule - * - * @throws ApiException if the remote call fails - */ - public function updateBackupSchedule( - $backupSchedule, - $updateMask, - array $optionalArgs = [] - ) { - $request = new UpdateBackupScheduleRequest(); - $requestParamHeaders = []; - $request->setBackupSchedule($backupSchedule); - $request->setUpdateMask($updateMask); - $requestParamHeaders[ - 'backup_schedule.name' - ] = $backupSchedule->getName(); - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'UpdateBackupSchedule', - BackupSchedule::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Updates a Cloud Spanner database. The returned - * [long-running operation][google.longrunning.Operation] can be used to track - * the progress of updating the database. If the named database does not - * exist, returns `NOT_FOUND`. - * - * While the operation is pending: - * - * * The database's - * [reconciling][google.spanner.admin.database.v1.Database.reconciling] - * field is set to true. - * * Cancelling the operation is best-effort. If the cancellation succeeds, - * the operation metadata's - * [cancel_time][google.spanner.admin.database.v1.UpdateDatabaseMetadata.cancel_time] - * is set, the updates are reverted, and the operation terminates with a - * `CANCELLED` status. - * * New UpdateDatabase requests will return a `FAILED_PRECONDITION` error - * until the pending operation is done (returns successfully or with - * error). - * * Reading the database via the API continues to give the pre-request - * values. - * - * Upon completion of the returned operation: - * - * * The new values are in effect and readable via the API. - * * The database's - * [reconciling][google.spanner.admin.database.v1.Database.reconciling] - * field becomes false. - * - * The returned [long-running operation][google.longrunning.Operation] will - * have a name of the format - * `projects//instances//databases//operations/` - * and can be used to track the database modification. The - * [metadata][google.longrunning.Operation.metadata] field type is - * [UpdateDatabaseMetadata][google.spanner.admin.database.v1.UpdateDatabaseMetadata]. - * The [response][google.longrunning.Operation.response] field type is - * [Database][google.spanner.admin.database.v1.Database], if successful. - * - * Sample code: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $database = new Database(); - * $updateMask = new FieldMask(); - * $operationResponse = $databaseAdminClient->updateDatabase($database, $updateMask); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $databaseAdminClient->updateDatabase($database, $updateMask); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'updateDatabase'); - * 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 Database $database Required. The database to update. - * The `name` field of the database is of the form - * `projects//instances//databases/`. - * @param FieldMask $updateMask Required. The list of fields to update. Currently, only - * `enable_drop_protection` field can be updated. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function updateDatabase( - $database, - $updateMask, - array $optionalArgs = [] - ) { - $request = new UpdateDatabaseRequest(); - $requestParamHeaders = []; - $request->setDatabase($database); - $request->setUpdateMask($updateMask); - $requestParamHeaders['database.name'] = $database->getName(); - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'UpdateDatabase', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * 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: - * ``` - * $databaseAdminClient = new DatabaseAdminClient(); - * try { - * $formattedDatabase = $databaseAdminClient->databaseName('[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) - * } - * // Alternatively: - * // 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 Required. 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-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 `ALREADY_EXISTS`. - * @type string $protoDescriptors - * Optional. Proto descriptors used by CREATE/ALTER PROTO BUNDLE statements. - * Contains a protobuf-serialized - * [google.protobuf.FileDescriptorSet](https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto). - * To generate it, [install](https://grpc.io/docs/protoc-installation/) and - * run `protoc` with --include_imports and --descriptor_set_out. For example, - * to generate for moon/shot/app.proto, run - * ``` - * $protoc --proto_path=/app_path --proto_path=/lib_path \ - * --include_imports \ - * --descriptor_set_out=descriptors.data \ - * moon/shot/app.proto - * ``` - * For more details, see protobuffer [self - * description](https://developers.google.com/protocol-buffers/docs/techniques#self-description). - * @type bool $throughputMode - * Optional. This field is exposed to be used by the Spanner Migration Tool. - * For more details, see - * [SMT](https://github.com/GoogleCloudPlatform/spanner-migration-tool). - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function updateDatabaseDdl( - $database, - $statements, - array $optionalArgs = [] - ) { - $request = new UpdateDatabaseDdlRequest(); - $requestParamHeaders = []; - $request->setDatabase($database); - $request->setStatements($statements); - $requestParamHeaders['database'] = $database; - if (isset($optionalArgs['operationId'])) { - $request->setOperationId($optionalArgs['operationId']); - } - - if (isset($optionalArgs['protoDescriptors'])) { - $request->setProtoDescriptors($optionalArgs['protoDescriptors']); - } - - if (isset($optionalArgs['throughputMode'])) { - $request->setThroughputMode($optionalArgs['throughputMode']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'UpdateDatabaseDdl', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } -} diff --git a/Spanner/src/Admin/Instance/V1/Gapic/InstanceAdminGapicClient.php b/Spanner/src/Admin/Instance/V1/Gapic/InstanceAdminGapicClient.php deleted file mode 100644 index 369c8441c203..000000000000 --- a/Spanner/src/Admin/Instance/V1/Gapic/InstanceAdminGapicClient.php +++ /dev/null @@ -1,2527 +0,0 @@ -projectName('[PROJECT]'); - * $instanceId = 'instance_id'; - * $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) - * } - * // Alternatively: - * // 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(); - * } - * ``` - * - * 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 parseName method to extract the individual identifiers - * contained within formatted names that are returned by the API. - * - * @deprecated Please use the new service client {@see \Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient}. - */ -class InstanceAdminGapicClient -{ - use GapicClientTrait; - - /** The name of the service. */ - const SERVICE_NAME = 'google.spanner.admin.instance.v1.InstanceAdmin'; - - /** - * The default address of the service. - * - * @deprecated SERVICE_ADDRESS_TEMPLATE should be used instead. - */ - const SERVICE_ADDRESS = 'spanner.googleapis.com'; - - /** The address template of the service. */ - private const SERVICE_ADDRESS_TEMPLATE = 'spanner.UNIVERSE_DOMAIN'; - - /** The default port of the service. */ - const DEFAULT_SERVICE_PORT = 443; - - /** The name of the code generator, to be included in the agent header. */ - const CODEGEN_NAME = 'gapic'; - - /** The default scopes required by the service. */ - public static $serviceScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.admin', - ]; - - private static $instanceNameTemplate; - - private static $instanceConfigNameTemplate; - - private static $instancePartitionNameTemplate; - - private static $projectNameTemplate; - - private static $pathTemplateMap; - - private $operationsClient; - - private static function getClientDefaults() - { - return [ - 'serviceName' => self::SERVICE_NAME, - 'apiEndpoint' => - self::SERVICE_ADDRESS . ':' . self::DEFAULT_SERVICE_PORT, - 'clientConfig' => - __DIR__ . '/../resources/instance_admin_client_config.json', - 'descriptorsConfigPath' => - __DIR__ . '/../resources/instance_admin_descriptor_config.php', - 'gcpApiConfigPath' => - __DIR__ . '/../resources/instance_admin_grpc_config.json', - 'credentialsConfig' => [ - 'defaultScopes' => self::$serviceScopes, - ], - 'transportConfig' => [ - 'rest' => [ - 'restClientConfigPath' => - __DIR__ . - '/../resources/instance_admin_rest_client_config.php', - ], - ], - ]; - } - - private static function getInstanceNameTemplate() - { - if (self::$instanceNameTemplate == null) { - self::$instanceNameTemplate = new PathTemplate( - 'projects/{project}/instances/{instance}' - ); - } - - return self::$instanceNameTemplate; - } - - private static function getInstanceConfigNameTemplate() - { - if (self::$instanceConfigNameTemplate == null) { - self::$instanceConfigNameTemplate = new PathTemplate( - 'projects/{project}/instanceConfigs/{instance_config}' - ); - } - - return self::$instanceConfigNameTemplate; - } - - private static function getInstancePartitionNameTemplate() - { - if (self::$instancePartitionNameTemplate == null) { - self::$instancePartitionNameTemplate = new PathTemplate( - 'projects/{project}/instances/{instance}/instancePartitions/{instance_partition}' - ); - } - - return self::$instancePartitionNameTemplate; - } - - private static function getProjectNameTemplate() - { - if (self::$projectNameTemplate == null) { - self::$projectNameTemplate = new PathTemplate('projects/{project}'); - } - - return self::$projectNameTemplate; - } - - private static function getPathTemplateMap() - { - if (self::$pathTemplateMap == null) { - self::$pathTemplateMap = [ - 'instance' => self::getInstanceNameTemplate(), - 'instanceConfig' => self::getInstanceConfigNameTemplate(), - 'instancePartition' => self::getInstancePartitionNameTemplate(), - 'project' => self::getProjectNameTemplate(), - ]; - } - - return self::$pathTemplateMap; - } - - /** - * Formats a string containing the fully-qualified path to represent a instance - * resource. - * - * @param string $project - * @param string $instance - * - * @return string The formatted instance resource. - */ - public static function instanceName($project, $instance) - { - return self::getInstanceNameTemplate()->render([ - 'project' => $project, - 'instance' => $instance, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a - * instance_config resource. - * - * @param string $project - * @param string $instanceConfig - * - * @return string The formatted instance_config resource. - */ - public static function instanceConfigName($project, $instanceConfig) - { - return self::getInstanceConfigNameTemplate()->render([ - 'project' => $project, - 'instance_config' => $instanceConfig, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a - * instance_partition resource. - * - * @param string $project - * @param string $instance - * @param string $instancePartition - * - * @return string The formatted instance_partition resource. - */ - public static function instancePartitionName( - $project, - $instance, - $instancePartition - ) { - return self::getInstancePartitionNameTemplate()->render([ - 'project' => $project, - 'instance' => $instance, - 'instance_partition' => $instancePartition, - ]); - } - - /** - * Formats a string containing the fully-qualified path to represent a project - * resource. - * - * @param string $project - * - * @return string The formatted project resource. - */ - public static function projectName($project) - { - return self::getProjectNameTemplate()->render([ - 'project' => $project, - ]); - } - - /** - * Parses a formatted name string and returns an associative array of the components in the name. - * The following name formats are supported: - * Template: Pattern - * - instance: projects/{project}/instances/{instance} - * - instanceConfig: projects/{project}/instanceConfigs/{instance_config} - * - instancePartition: projects/{project}/instances/{instance}/instancePartitions/{instance_partition} - * - project: projects/{project} - * - * The optional $template argument can be supplied to specify a particular pattern, - * and must match one of the templates listed above. If no $template argument is - * provided, or if the $template argument does not match one of the templates - * listed, then parseName will check each of the supported templates, and return - * the first match. - * - * @param string $formattedName The formatted name string - * @param string $template Optional name of template to match - * - * @return array An associative array from name component IDs to component values. - * - * @throws ValidationException If $formattedName could not be matched. - */ - public static function parseName($formattedName, $template = null) - { - $templateMap = self::getPathTemplateMap(); - if ($template) { - if (!isset($templateMap[$template])) { - throw new ValidationException( - "Template name $template does not exist" - ); - } - - return $templateMap[$template]->match($formattedName); - } - - foreach ($templateMap as $templateName => $pathTemplate) { - try { - return $pathTemplate->match($formattedName); - } catch (ValidationException $ex) { - // Swallow the exception to continue trying other path templates - } - } - - throw new ValidationException( - "Input did not match any known format. Input: $formattedName" - ); - } - - /** - * Return an OperationsClient object with the same endpoint as $this. - * - * @return 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 OperationResponse - */ - public function resumeOperation($operationName, $methodName = null) - { - $options = isset($this->descriptors[$methodName]['longRunning']) - ? $this->descriptors[$methodName]['longRunning'] - : []; - $operation = new OperationResponse( - $operationName, - $this->getOperationsClient(), - $options - ); - $operation->reload(); - return $operation; - } - - /** - * Constructor. - * - * @param array $options { - * Optional. Options for configuring the service API wrapper. - * - * @type string $apiEndpoint - * The address of the API remote host. May optionally include the port, formatted - * as ":". Default 'spanner.googleapis.com:443'. - * @type string|array|FetchAuthTokenInterface|CredentialsWrapper $credentials - * The credentials to be used by the client to authorize API calls. This option - * accepts either a path to a credentials file, or a decoded credentials file as a - * PHP array. - * *Advanced usage*: In addition, this option can also accept a pre-constructed - * {@see \Google\Auth\FetchAuthTokenInterface} object or - * {@see \Google\ApiCore\CredentialsWrapper} object. Note that when one of these - * objects are provided, any settings in $credentialsConfig will be ignored. - * @type array $credentialsConfig - * Options used to configure credentials, including auth token caching, for the - * client. For a full list of supporting configuration options, see - * {@see \Google\ApiCore\CredentialsWrapper::build()} . - * @type bool $disableRetries - * Determines whether or not retries defined by the client configuration should be - * disabled. Defaults to `false`. - * @type string|array $clientConfig - * Client method configuration, including retry settings. This option can be either - * a path to a JSON file, or a PHP array containing the decoded JSON data. By - * default this settings points to the default client config file, which is - * provided in the resources folder. - * @type string|TransportInterface $transport - * The transport used for executing network requests. May be either the string - * `rest` or `grpc`. Defaults to `grpc` if gRPC support is detected on the system. - * *Advanced usage*: Additionally, it is possible to pass in an already - * instantiated {@see \Google\ApiCore\Transport\TransportInterface} object. Note - * that when this object is provided, any settings in $transportConfig, and any - * $apiEndpoint setting, will be ignored. - * @type array $transportConfig - * Configuration options that will be used to construct the transport. Options for - * each supported transport type should be passed in a key for that transport. For - * example: - * $transportConfig = [ - * 'grpc' => [...], - * 'rest' => [...], - * ]; - * See the {@see \Google\ApiCore\Transport\GrpcTransport::build()} and - * {@see \Google\ApiCore\Transport\RestTransport::build()} methods for the - * supported options. - * @type callable $clientCertSource - * A callable which returns the client cert as a string. This can be used to - * provide a certificate and private key to the transport layer for mTLS. - * } - * - * @throws ValidationException - */ - public function __construct(array $options = []) - { - $clientOptions = $this->buildClientOptions($options); - $this->setClientOptions($clientOptions); - $this->operationsClient = $this->createOperationsClient($clientOptions); - } - - /** - * Creates an instance and begins preparing it to begin serving. The - * returned long-running 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 will - * have a name of the format `/operations/` and - * can be used to track creation of the instance. The - * metadata field type is - * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. - * The response field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedParent = $instanceAdminClient->projectName('[PROJECT]'); - * $instanceId = 'instance_id'; - * $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) - * } - * // Alternatively: - * // 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 2 and 64 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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function createInstance( - $parent, - $instanceId, - $instance, - array $optionalArgs = [] - ) { - $request = new CreateInstanceRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $request->setInstanceId($instanceId); - $request->setInstance($instance); - $requestParamHeaders['parent'] = $parent; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'CreateInstance', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * Creates an instance configuration and begins preparing it to be used. The - * returned long-running operation - * can be used to track the progress of preparing the new - * instance configuration. The instance configuration name is assigned by the - * caller. If the named instance configuration already exists, - * `CreateInstanceConfig` returns `ALREADY_EXISTS`. - * - * Immediately after the request returns: - * - * * The instance configuration is readable via the API, with all requested - * attributes. The instance configuration's - * [reconciling][google.spanner.admin.instance.v1.InstanceConfig.reconciling] - * field is set to true. Its state is `CREATING`. - * - * While the operation is pending: - * - * * Cancelling the operation renders the instance configuration immediately - * unreadable via the API. - * * Except for deleting the creating resource, all other attempts to modify - * the instance configuration are rejected. - * - * Upon completion of the returned operation: - * - * * Instances can be created using the instance configuration. - * * The instance configuration's - * [reconciling][google.spanner.admin.instance.v1.InstanceConfig.reconciling] - * field becomes false. Its state becomes `READY`. - * - * The returned long-running operation will - * have a name of the format - * `/operations/` and can be used to track - * creation of the instance configuration. The - * metadata field type is - * [CreateInstanceConfigMetadata][google.spanner.admin.instance.v1.CreateInstanceConfigMetadata]. - * The response field type is - * [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig], if - * successful. - * - * Authorization requires `spanner.instanceConfigs.create` permission on - * the resource - * [parent][google.spanner.admin.instance.v1.CreateInstanceConfigRequest.parent]. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedParent = $instanceAdminClient->projectName('[PROJECT]'); - * $instanceConfigId = 'instance_config_id'; - * $instanceConfig = new InstanceConfig(); - * $operationResponse = $instanceAdminClient->createInstanceConfig($formattedParent, $instanceConfigId, $instanceConfig); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $instanceAdminClient->createInstanceConfig($formattedParent, $instanceConfigId, $instanceConfig); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'createInstanceConfig'); - * 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 - * configuration. Values are of the form `projects/`. - * @param string $instanceConfigId Required. The ID of the instance configuration to create. Valid identifiers - * are of the form `custom-[-a-z0-9]*[a-z0-9]` and must be between 2 and 64 - * characters in length. The `custom-` prefix is required to avoid name - * conflicts with Google-managed configurations. - * @param InstanceConfig $instanceConfig Required. The `InstanceConfig` proto of the configuration to create. - * `instance_config.name` must be - * `/instanceConfigs/`. - * `instance_config.base_config` must be a Google-managed configuration name, - * e.g. /instanceConfigs/us-east1, /instanceConfigs/nam3. - * @param array $optionalArgs { - * Optional. - * - * @type bool $validateOnly - * An option to validate, but not actually execute, a request, - * and provide the same response. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function createInstanceConfig( - $parent, - $instanceConfigId, - $instanceConfig, - array $optionalArgs = [] - ) { - $request = new CreateInstanceConfigRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $request->setInstanceConfigId($instanceConfigId); - $request->setInstanceConfig($instanceConfig); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['validateOnly'])) { - $request->setValidateOnly($optionalArgs['validateOnly']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'CreateInstanceConfig', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * Creates an instance partition and begins preparing it to be used. The - * returned long-running operation - * can be used to track the progress of preparing the new instance partition. - * The instance partition name is assigned by the caller. If the named - * instance partition already exists, `CreateInstancePartition` returns - * `ALREADY_EXISTS`. - * - * Immediately upon completion of this request: - * - * * The instance partition 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 partition immediately - * unreadable via the API. - * * The instance partition can be deleted. - * * All other attempts to modify the instance partition 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 start using this instance partition. - * * The instance partition's allocated resource levels are readable via the - * API. - * * The instance partition's state becomes `READY`. - * - * The returned long-running operation will - * have a name of the format - * `/operations/` and can be used to - * track creation of the instance partition. The - * metadata field type is - * [CreateInstancePartitionMetadata][google.spanner.admin.instance.v1.CreateInstancePartitionMetadata]. - * The response field type is - * [InstancePartition][google.spanner.admin.instance.v1.InstancePartition], if - * successful. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedParent = $instanceAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * $instancePartitionId = 'instance_partition_id'; - * $instancePartition = new InstancePartition(); - * $operationResponse = $instanceAdminClient->createInstancePartition($formattedParent, $instancePartitionId, $instancePartition); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $instanceAdminClient->createInstancePartition($formattedParent, $instancePartitionId, $instancePartition); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'createInstancePartition'); - * 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 instance in which to create the instance - * partition. Values are of the form - * `projects//instances/`. - * @param string $instancePartitionId Required. The ID of the instance partition to create. Valid identifiers are - * of the form `[a-z][-a-z0-9]*[a-z0-9]` and must be between 2 and 64 - * characters in length. - * @param InstancePartition $instancePartition Required. The instance partition to create. The instance_partition.name may - * be omitted, but if specified must be - * `/instancePartitions/`. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function createInstancePartition( - $parent, - $instancePartitionId, - $instancePartition, - array $optionalArgs = [] - ) { - $request = new CreateInstancePartitionRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $request->setInstancePartitionId($instancePartitionId); - $request->setInstancePartition($instancePartition); - $requestParamHeaders['parent'] = $parent; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'CreateInstancePartition', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * 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: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedName = $instanceAdminClient->instanceName('[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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @throws ApiException if the remote call fails - */ - public function deleteInstance($name, array $optionalArgs = []) - { - $request = new DeleteInstanceRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'DeleteInstance', - GPBEmpty::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Deletes the instance configuration. Deletion is only allowed when no - * instances are using the configuration. If any instances are using - * the configuration, returns `FAILED_PRECONDITION`. - * - * Only user-managed configurations can be deleted. - * - * Authorization requires `spanner.instanceConfigs.delete` permission on - * the resource [name][google.spanner.admin.instance.v1.InstanceConfig.name]. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedName = $instanceAdminClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - * $instanceAdminClient->deleteInstanceConfig($formattedName); - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $name Required. The name of the instance configuration to be deleted. - * Values are of the form - * `projects//instanceConfigs/` - * @param array $optionalArgs { - * Optional. - * - * @type string $etag - * Used for optimistic concurrency control as a way to help prevent - * simultaneous deletes of an instance configuration from overwriting each - * other. If not empty, the API - * only deletes the instance configuration when the etag provided matches the - * current status of the requested instance configuration. Otherwise, deletes - * the instance configuration without checking the current status of the - * requested instance configuration. - * @type bool $validateOnly - * An option to validate, but not actually execute, a request, - * and provide the same response. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @throws ApiException if the remote call fails - */ - public function deleteInstanceConfig($name, array $optionalArgs = []) - { - $request = new DeleteInstanceConfigRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - if (isset($optionalArgs['etag'])) { - $request->setEtag($optionalArgs['etag']); - } - - if (isset($optionalArgs['validateOnly'])) { - $request->setValidateOnly($optionalArgs['validateOnly']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'DeleteInstanceConfig', - GPBEmpty::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Deletes an existing instance partition. Requires that the - * instance partition is not used by any database or backup and is not the - * default instance partition of an instance. - * - * Authorization requires `spanner.instancePartitions.delete` permission on - * the resource - * [name][google.spanner.admin.instance.v1.InstancePartition.name]. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedName = $instanceAdminClient->instancePartitionName('[PROJECT]', '[INSTANCE]', '[INSTANCE_PARTITION]'); - * $instanceAdminClient->deleteInstancePartition($formattedName); - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $name Required. The name of the instance partition to be deleted. - * Values are of the form - * `projects/{project}/instances/{instance}/instancePartitions/{instance_partition}` - * @param array $optionalArgs { - * Optional. - * - * @type string $etag - * Optional. If not empty, the API only deletes the instance partition when - * the etag provided matches the current status of the requested instance - * partition. Otherwise, deletes the instance partition without checking the - * current status of the requested instance partition. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @throws ApiException if the remote call fails - */ - public function deleteInstancePartition($name, array $optionalArgs = []) - { - $request = new DeleteInstancePartitionRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - if (isset($optionalArgs['etag'])) { - $request->setEtag($optionalArgs['etag']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'DeleteInstancePartition', - GPBEmpty::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $resource = 'resource'; - * $response = $instanceAdminClient->getIamPolicy($resource); - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $resource REQUIRED: The resource for which the policy is being requested. - * See the operation documentation for the appropriate value for this field. - * @param array $optionalArgs { - * Optional. - * - * @type GetPolicyOptions $options - * OPTIONAL: A `GetPolicyOptions` object for specifying options to - * `GetIamPolicy`. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Iam\V1\Policy - * - * @throws ApiException if the remote call fails - */ - public function getIamPolicy($resource, array $optionalArgs = []) - { - $request = new GetIamPolicyRequest(); - $requestParamHeaders = []; - $request->setResource($resource); - $requestParamHeaders['resource'] = $resource; - if (isset($optionalArgs['options'])) { - $request->setOptions($optionalArgs['options']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetIamPolicy', - Policy::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Gets information about a particular instance. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedName = $instanceAdminClient->instanceName('[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 FieldMask $fieldMask - * If field_mask is present, specifies the subset of - * [Instance][google.spanner.admin.instance.v1.Instance] fields that should be - * returned. If absent, all - * [Instance][google.spanner.admin.instance.v1.Instance] fields are returned. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Instance\V1\Instance - * - * @throws ApiException if the remote call fails - */ - public function getInstance($name, array $optionalArgs = []) - { - $request = new GetInstanceRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - if (isset($optionalArgs['fieldMask'])) { - $request->setFieldMask($optionalArgs['fieldMask']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetInstance', - Instance::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Gets information about a particular instance configuration. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedName = $instanceAdminClient->instanceConfigName('[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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig - * - * @throws ApiException if the remote call fails - */ - public function getInstanceConfig($name, array $optionalArgs = []) - { - $request = new GetInstanceConfigRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetInstanceConfig', - InstanceConfig::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Gets information about a particular instance partition. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedName = $instanceAdminClient->instancePartitionName('[PROJECT]', '[INSTANCE]', '[INSTANCE_PARTITION]'); - * $response = $instanceAdminClient->getInstancePartition($formattedName); - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $name Required. The name of the requested instance partition. Values are of - * the form - * `projects/{project}/instances/{instance}/instancePartitions/{instance_partition}`. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\Admin\Instance\V1\InstancePartition - * - * @throws ApiException if the remote call fails - */ - public function getInstancePartition($name, array $optionalArgs = []) - { - $request = new GetInstancePartitionRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetInstancePartition', - InstancePartition::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Lists the user-managed instance configuration long-running - * operations in the given project. An instance - * configuration operation has a name of the form - * `projects//instanceConfigs//operations/`. - * The long-running operation - * metadata field type - * `metadata.type_url` describes the type of the metadata. Operations returned - * include those that have completed/failed/canceled within the last 7 days, - * and pending operations. Operations returned are ordered by - * `operation.metadata.value.start_time` in descending order starting - * from the most recently started operation. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedParent = $instanceAdminClient->projectName('[PROJECT]'); - * // Iterate over pages of elements - * $pagedResponse = $instanceAdminClient->listInstanceConfigOperations($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $instanceAdminClient->listInstanceConfigOperations($formattedParent); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. The project of the instance configuration operations. - * Values are of the form `projects/`. - * @param array $optionalArgs { - * Optional. - * - * @type string $filter - * An expression that filters the list of returned operations. - * - * A filter expression consists of a field name, a - * comparison operator, and a value for filtering. - * The value must be a string, a number, or a boolean. The comparison operator - * must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. - * Colon `:` is the contains operator. Filter rules are not case sensitive. - * - * The following fields in the Operation are eligible for filtering: - * - * * `name` - The name of the long-running operation - * * `done` - False if the operation is in progress, else true. - * * `metadata.@type` - the type of metadata. For example, the type string - * for - * [CreateInstanceConfigMetadata][google.spanner.admin.instance.v1.CreateInstanceConfigMetadata] - * is - * `type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata`. - * * `metadata.` - any field in metadata.value. - * `metadata.@type` must be specified first, if filtering on metadata - * fields. - * * `error` - Error associated with the long-running operation. - * * `response.@type` - the type of response. - * * `response.` - any field in response.value. - * - * You can combine multiple expressions by enclosing each expression in - * parentheses. By default, expressions are combined with AND logic. However, - * you can specify AND, OR, and NOT logic explicitly. - * - * Here are a few examples: - * - * * `done:true` - The operation is complete. - * * `(metadata.@type=` \ - * `type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata) - * AND` \ - * `(metadata.instance_config.name:custom-config) AND` \ - * `(metadata.progress.start_time < \"2021-03-28T14:50:00Z\") AND` \ - * `(error:*)` - Return operations where: - * * The operation's metadata type is - * [CreateInstanceConfigMetadata][google.spanner.admin.instance.v1.CreateInstanceConfigMetadata]. - * * The instance configuration name contains "custom-config". - * * The operation started before 2021-03-28T14:50:00Z. - * * The operation resulted in an error. - * @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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listInstanceConfigOperations( - $parent, - array $optionalArgs = [] - ) { - $request = new ListInstanceConfigOperationsRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['filter'])) { - $request->setFilter($optionalArgs['filter']); - } - - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListInstanceConfigOperations', - $optionalArgs, - ListInstanceConfigOperationsResponse::class, - $request - ); - } - - /** - * Lists the supported instance configurations for a given project. - * - * Returns both Google-managed configurations and user-managed - * configurations. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedParent = $instanceAdminClient->projectName('[PROJECT]'); - * // Iterate over pages of elements - * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent); - * foreach ($pagedResponse->iterateAllElements() 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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listInstanceConfigs($parent, array $optionalArgs = []) - { - $request = new ListInstanceConfigsRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListInstanceConfigs', - $optionalArgs, - ListInstanceConfigsResponse::class, - $request - ); - } - - /** - * Lists instance partition long-running operations in the given instance. - * An instance partition operation has a name of the form - * `projects//instances//instancePartitions//operations/`. - * The long-running operation - * metadata field type - * `metadata.type_url` describes the type of the metadata. Operations returned - * include those that have completed/failed/canceled within the last 7 days, - * and pending operations. Operations returned are ordered by - * `operation.metadata.value.start_time` in descending order starting from the - * most recently started operation. - * - * Authorization requires `spanner.instancePartitionOperations.list` - * permission on the resource - * [parent][google.spanner.admin.instance.v1.ListInstancePartitionOperationsRequest.parent]. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedParent = $instanceAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * // Iterate over pages of elements - * $pagedResponse = $instanceAdminClient->listInstancePartitionOperations($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $instanceAdminClient->listInstancePartitionOperations($formattedParent); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. The parent instance of the instance partition operations. - * Values are of the form `projects//instances/`. - * @param array $optionalArgs { - * Optional. - * - * @type string $filter - * Optional. An expression that filters the list of returned operations. - * - * A filter expression consists of a field name, a - * comparison operator, and a value for filtering. - * The value must be a string, a number, or a boolean. The comparison operator - * must be one of: `<`, `>`, `<=`, `>=`, `!=`, `=`, or `:`. - * Colon `:` is the contains operator. Filter rules are not case sensitive. - * - * The following fields in the Operation are eligible for filtering: - * - * * `name` - The name of the long-running operation - * * `done` - False if the operation is in progress, else true. - * * `metadata.@type` - the type of metadata. For example, the type string - * for - * [CreateInstancePartitionMetadata][google.spanner.admin.instance.v1.CreateInstancePartitionMetadata] - * is - * `type.googleapis.com/google.spanner.admin.instance.v1.CreateInstancePartitionMetadata`. - * * `metadata.` - any field in metadata.value. - * `metadata.@type` must be specified first, if filtering on metadata - * fields. - * * `error` - Error associated with the long-running operation. - * * `response.@type` - the type of response. - * * `response.` - any field in response.value. - * - * You can combine multiple expressions by enclosing each expression in - * parentheses. By default, expressions are combined with AND logic. However, - * you can specify AND, OR, and NOT logic explicitly. - * - * Here are a few examples: - * - * * `done:true` - The operation is complete. - * * `(metadata.@type=` \ - * `type.googleapis.com/google.spanner.admin.instance.v1.CreateInstancePartitionMetadata) - * AND` \ - * `(metadata.instance_partition.name:custom-instance-partition) AND` \ - * `(metadata.start_time < \"2021-03-28T14:50:00Z\") AND` \ - * `(error:*)` - Return operations where: - * * The operation's metadata type is - * [CreateInstancePartitionMetadata][google.spanner.admin.instance.v1.CreateInstancePartitionMetadata]. - * * The instance partition name contains "custom-instance-partition". - * * The operation started before 2021-03-28T14:50:00Z. - * * The operation resulted in an error. - * @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 Timestamp $instancePartitionDeadline - * Optional. Deadline used while retrieving metadata for instance partition - * operations. Instance partitions whose operation metadata cannot be - * retrieved within this deadline will be added to - * [unreachable_instance_partitions][google.spanner.admin.instance.v1.ListInstancePartitionOperationsResponse.unreachable_instance_partitions] - * in - * [ListInstancePartitionOperationsResponse][google.spanner.admin.instance.v1.ListInstancePartitionOperationsResponse]. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listInstancePartitionOperations( - $parent, - array $optionalArgs = [] - ) { - $request = new ListInstancePartitionOperationsRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['filter'])) { - $request->setFilter($optionalArgs['filter']); - } - - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - if (isset($optionalArgs['instancePartitionDeadline'])) { - $request->setInstancePartitionDeadline( - $optionalArgs['instancePartitionDeadline'] - ); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListInstancePartitionOperations', - $optionalArgs, - ListInstancePartitionOperationsResponse::class, - $request - ); - } - - /** - * Lists all instance partitions for the given instance. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedParent = $instanceAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * // Iterate over pages of elements - * $pagedResponse = $instanceAdminClient->listInstancePartitions($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $instanceAdminClient->listInstancePartitions($formattedParent); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $parent Required. The instance whose instance partitions should be listed. Values - * are of the form `projects//instances/`. Use `{instance} - * = '-'` to list instance partitions for all Instances in a project, e.g., - * `projects/myproject/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 Timestamp $instancePartitionDeadline - * Optional. Deadline used while retrieving metadata for instance partitions. - * Instance partitions whose metadata cannot be retrieved within this deadline - * will be added to - * [unreachable][google.spanner.admin.instance.v1.ListInstancePartitionsResponse.unreachable] - * in - * [ListInstancePartitionsResponse][google.spanner.admin.instance.v1.ListInstancePartitionsResponse]. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listInstancePartitions($parent, array $optionalArgs = []) - { - $request = new ListInstancePartitionsRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $parent; - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - if (isset($optionalArgs['instancePartitionDeadline'])) { - $request->setInstancePartitionDeadline( - $optionalArgs['instancePartitionDeadline'] - ); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListInstancePartitions', - $optionalArgs, - ListInstancePartitionsResponse::class, - $request - ); - } - - /** - * Lists all instances in the given project. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedParent = $instanceAdminClient->projectName('[PROJECT]'); - * // Iterate over pages of elements - * $pagedResponse = $instanceAdminClient->listInstances($formattedParent); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $instanceAdminClient->listInstances($formattedParent); - * foreach ($pagedResponse->iterateAllElements() 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 Timestamp $instanceDeadline - * Deadline used while retrieving metadata for instances. - * Instances whose metadata cannot be retrieved within this deadline will be - * added to - * [unreachable][google.spanner.admin.instance.v1.ListInstancesResponse.unreachable] - * in - * [ListInstancesResponse][google.spanner.admin.instance.v1.ListInstancesResponse]. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listInstances($parent, array $optionalArgs = []) - { - $request = new ListInstancesRequest(); - $requestParamHeaders = []; - $request->setParent($parent); - $requestParamHeaders['parent'] = $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']); - } - - if (isset($optionalArgs['instanceDeadline'])) { - $request->setInstanceDeadline($optionalArgs['instanceDeadline']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListInstances', - $optionalArgs, - ListInstancesResponse::class, - $request - ); - } - - /** - * Moves an instance to the target instance configuration. You can use the - * returned long-running operation to track - * the progress of moving the instance. - * - * `MoveInstance` returns `FAILED_PRECONDITION` if the instance meets any of - * the following criteria: - * - * * Is undergoing a move to a different instance configuration - * * Has backups - * * Has an ongoing update - * * Contains any CMEK-enabled databases - * * Is a free trial instance - * - * While the operation is pending: - * - * * All other attempts to modify the instance, including changes to its - * compute capacity, are rejected. - * * The following database and backup admin operations are rejected: - * - * * `DatabaseAdmin.CreateDatabase` - * * `DatabaseAdmin.UpdateDatabaseDdl` (disabled if default_leader is - * specified in the request.) - * * `DatabaseAdmin.RestoreDatabase` - * * `DatabaseAdmin.CreateBackup` - * * `DatabaseAdmin.CopyBackup` - * - * * Both the source and target instance configurations are subject to - * hourly compute and storage charges. - * * The instance might experience higher read-write latencies and a higher - * transaction abort rate. However, moving an instance doesn't cause any - * downtime. - * - * The returned long-running operation has - * a name of the format - * `/operations/` and can be used to track - * the move instance operation. The - * metadata field type is - * [MoveInstanceMetadata][google.spanner.admin.instance.v1.MoveInstanceMetadata]. - * The response field type is - * [Instance][google.spanner.admin.instance.v1.Instance], - * if successful. - * Cancelling the operation sets its metadata's - * [cancel_time][google.spanner.admin.instance.v1.MoveInstanceMetadata.cancel_time]. - * Cancellation is not immediate because it involves moving any data - * previously moved to the target instance configuration back to the original - * instance configuration. You can use this operation to track the progress of - * the cancellation. Upon successful completion of the cancellation, the - * operation terminates with `CANCELLED` status. - * - * If not cancelled, upon completion of the returned operation: - * - * * The instance successfully moves to the target instance - * configuration. - * * You are billed for compute and storage in target instance - * configuration. - * - * Authorization requires the `spanner.instances.update` permission on - * the resource [instance][google.spanner.admin.instance.v1.Instance]. - * - * For more details, see - * [Move an instance](https://cloud.google.com/spanner/docs/move-instance). - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $formattedName = $instanceAdminClient->instanceName('[PROJECT]', '[INSTANCE]'); - * $formattedTargetConfig = $instanceAdminClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - * $operationResponse = $instanceAdminClient->moveInstance($formattedName, $formattedTargetConfig); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $instanceAdminClient->moveInstance($formattedName, $formattedTargetConfig); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'moveInstance'); - * 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 $name Required. The instance to move. - * Values are of the form `projects//instances/`. - * @param string $targetConfig Required. The target instance configuration where to move the instance. - * Values are of the form `projects//instanceConfigs/`. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function moveInstance($name, $targetConfig, array $optionalArgs = []) - { - $request = new MoveInstanceRequest(); - $requestParamHeaders = []; - $request->setName($name); - $request->setTargetConfig($targetConfig); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'MoveInstance', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * 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: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $resource = 'resource'; - * $policy = new Policy(); - * $response = $instanceAdminClient->setIamPolicy($resource, $policy); - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $resource REQUIRED: The resource for which the policy is being specified. - * See the operation documentation for the appropriate value for this field. - * @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 FieldMask $updateMask - * OPTIONAL: A FieldMask specifying which fields of the policy to modify. Only - * the fields in the mask will be modified. If no mask is provided, the - * following default mask is used: - * - * `paths: "bindings, etag"` - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Iam\V1\Policy - * - * @throws ApiException if the remote call fails - */ - public function setIamPolicy($resource, $policy, array $optionalArgs = []) - { - $request = new SetIamPolicyRequest(); - $requestParamHeaders = []; - $request->setResource($resource); - $request->setPolicy($policy); - $requestParamHeaders['resource'] = $resource; - if (isset($optionalArgs['updateMask'])) { - $request->setUpdateMask($optionalArgs['updateMask']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'SetIamPolicy', - Policy::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $resource = 'resource'; - * $permissions = []; - * $response = $instanceAdminClient->testIamPermissions($resource, $permissions); - * } finally { - * $instanceAdminClient->close(); - * } - * ``` - * - * @param string $resource REQUIRED: The resource for which the policy detail is being requested. - * See the operation documentation for the appropriate value for this field. - * @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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Iam\V1\TestIamPermissionsResponse - * - * @throws ApiException if the remote call fails - */ - public function testIamPermissions( - $resource, - $permissions, - array $optionalArgs = [] - ) { - $request = new TestIamPermissionsRequest(); - $requestParamHeaders = []; - $request->setResource($resource); - $request->setPermissions($permissions); - $requestParamHeaders['resource'] = $resource; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'TestIamPermissions', - TestIamPermissionsResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Updates an instance, and begins allocating or releasing resources - * as requested. The returned long-running 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 will - * have a name of the format `/operations/` and - * can be used to track the instance modification. The - * metadata field type is - * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. - * The response field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if successful. - * - * Authorization requires `spanner.instances.update` permission on - * the resource [name][google.spanner.admin.instance.v1.Instance.name]. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $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) - * } - * // Alternatively: - * // 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 - * [field_mask][google.spanner.admin.instance.v1.UpdateInstanceRequest.field_mask] - * need be included. - * @param FieldMask $fieldMask Required. A mask specifying which fields in - * [Instance][google.spanner.admin.instance.v1.Instance] should be updated. - * The field mask must always be specified; this prevents any future fields in - * [Instance][google.spanner.admin.instance.v1.Instance] from being erased - * accidentally by clients that do not know about them. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function updateInstance( - $instance, - $fieldMask, - array $optionalArgs = [] - ) { - $request = new UpdateInstanceRequest(); - $requestParamHeaders = []; - $request->setInstance($instance); - $request->setFieldMask($fieldMask); - $requestParamHeaders['instance.name'] = $instance->getName(); - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'UpdateInstance', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * Updates an instance configuration. The returned - * long-running operation can be used to track - * the progress of updating the instance. If the named instance configuration - * does not exist, returns `NOT_FOUND`. - * - * Only user-managed configurations can be updated. - * - * Immediately after the request returns: - * - * * The instance configuration's - * [reconciling][google.spanner.admin.instance.v1.InstanceConfig.reconciling] - * field is set to true. - * - * While the operation is pending: - * - * * Cancelling the operation sets its metadata's - * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata.cancel_time]. - * The operation is guaranteed to succeed at undoing all changes, after - * which point it terminates with a `CANCELLED` status. - * * All other attempts to modify the instance configuration are rejected. - * * Reading the instance configuration via the API continues to give the - * pre-request values. - * - * Upon completion of the returned operation: - * - * * Creating instances using the instance configuration uses the new - * values. - * * The new values of the instance configuration are readable via the API. - * * The instance configuration's - * [reconciling][google.spanner.admin.instance.v1.InstanceConfig.reconciling] - * field becomes false. - * - * The returned long-running operation will - * have a name of the format - * `/operations/` and can be used to track - * the instance configuration modification. The - * metadata field type is - * [UpdateInstanceConfigMetadata][google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata]. - * The response field type is - * [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig], if - * successful. - * - * Authorization requires `spanner.instanceConfigs.update` permission on - * the resource [name][google.spanner.admin.instance.v1.InstanceConfig.name]. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $instanceConfig = new InstanceConfig(); - * $updateMask = new FieldMask(); - * $operationResponse = $instanceAdminClient->updateInstanceConfig($instanceConfig, $updateMask); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $instanceAdminClient->updateInstanceConfig($instanceConfig, $updateMask); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'updateInstanceConfig'); - * 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 InstanceConfig $instanceConfig Required. The user instance configuration to update, which must always - * include the instance configuration name. Otherwise, only fields mentioned - * in - * [update_mask][google.spanner.admin.instance.v1.UpdateInstanceConfigRequest.update_mask] - * need be included. To prevent conflicts of concurrent updates, - * [etag][google.spanner.admin.instance.v1.InstanceConfig.reconciling] can - * be used. - * @param FieldMask $updateMask Required. A mask specifying which fields in - * [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig] should be - * updated. The field mask must always be specified; this prevents any future - * fields in [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig] - * from being erased accidentally by clients that do not know about them. Only - * display_name and labels can be updated. - * @param array $optionalArgs { - * Optional. - * - * @type bool $validateOnly - * An option to validate, but not actually execute, a request, - * and provide the same response. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function updateInstanceConfig( - $instanceConfig, - $updateMask, - array $optionalArgs = [] - ) { - $request = new UpdateInstanceConfigRequest(); - $requestParamHeaders = []; - $request->setInstanceConfig($instanceConfig); - $request->setUpdateMask($updateMask); - $requestParamHeaders[ - 'instance_config.name' - ] = $instanceConfig->getName(); - if (isset($optionalArgs['validateOnly'])) { - $request->setValidateOnly($optionalArgs['validateOnly']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'UpdateInstanceConfig', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } - - /** - * Updates an instance partition, and begins allocating or releasing resources - * as requested. The returned long-running operation can be used to track the - * progress of updating the instance partition. If the named instance - * partition does not exist, returns `NOT_FOUND`. - * - * Immediately upon completion of this request: - * - * * For resource types for which a decrease in the instance partition'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.UpdateInstancePartitionMetadata.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 partition are rejected. - * * Reading the instance partition 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 - * partition's tables. - * * The instance partition's new resource levels are readable via the API. - * - * The returned long-running operation will - * have a name of the format - * `/operations/` and can be used to - * track the instance partition modification. The - * metadata field type is - * [UpdateInstancePartitionMetadata][google.spanner.admin.instance.v1.UpdateInstancePartitionMetadata]. - * The response field type is - * [InstancePartition][google.spanner.admin.instance.v1.InstancePartition], if - * successful. - * - * Authorization requires `spanner.instancePartitions.update` permission on - * the resource - * [name][google.spanner.admin.instance.v1.InstancePartition.name]. - * - * Sample code: - * ``` - * $instanceAdminClient = new InstanceAdminClient(); - * try { - * $instancePartition = new InstancePartition(); - * $fieldMask = new FieldMask(); - * $operationResponse = $instanceAdminClient->updateInstancePartition($instancePartition, $fieldMask); - * $operationResponse->pollUntilComplete(); - * if ($operationResponse->operationSucceeded()) { - * $result = $operationResponse->getResult(); - * // doSomethingWith($result) - * } else { - * $error = $operationResponse->getError(); - * // handleError($error) - * } - * // Alternatively: - * // start the operation, keep the operation name, and resume later - * $operationResponse = $instanceAdminClient->updateInstancePartition($instancePartition, $fieldMask); - * $operationName = $operationResponse->getName(); - * // ... do other work - * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'updateInstancePartition'); - * 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 InstancePartition $instancePartition Required. The instance partition to update, which must always include the - * instance partition name. Otherwise, only fields mentioned in - * [field_mask][google.spanner.admin.instance.v1.UpdateInstancePartitionRequest.field_mask] - * need be included. - * @param FieldMask $fieldMask Required. A mask specifying which fields in - * [InstancePartition][google.spanner.admin.instance.v1.InstancePartition] - * should be updated. The field mask must always be specified; this prevents - * any future fields in - * [InstancePartition][google.spanner.admin.instance.v1.InstancePartition] - * from being erased accidentally by clients that do not know about them. - * @param array $optionalArgs { - * Optional. - * - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\OperationResponse - * - * @throws ApiException if the remote call fails - */ - public function updateInstancePartition( - $instancePartition, - $fieldMask, - array $optionalArgs = [] - ) { - $request = new UpdateInstancePartitionRequest(); - $requestParamHeaders = []; - $request->setInstancePartition($instancePartition); - $request->setFieldMask($fieldMask); - $requestParamHeaders[ - 'instance_partition.name' - ] = $instancePartition->getName(); - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startOperationsCall( - 'UpdateInstancePartition', - $optionalArgs, - $request, - $this->getOperationsClient() - )->wait(); - } -} diff --git a/Spanner/src/Admin/Instance/V1/InstanceAdminClient.php b/Spanner/src/Admin/Instance/V1/InstanceAdminClient.php deleted file mode 100644 index 14a15252ca65..000000000000 --- a/Spanner/src/Admin/Instance/V1/InstanceAdminClient.php +++ /dev/null @@ -1,42 +0,0 @@ - 'my-project']); * $database = $spanner->connect('my-instance', 'my-database'); * * $arrayType = new ArrayType(Database::TYPE_STRING); @@ -61,18 +61,11 @@ */ class ArrayType { - /** - * @var int|null - */ - private $type; - - /** - * @var StructType|null - */ - private $structType; + private int|string|null $type; + private StructType|null $structType; /** - * @param int|string|null|StructType $type A value type code or nested struct + * @param int|string|StructType|null $type A value type code or nested struct * definition. Accepted integer and string values are defined as constants on * {@see \Google\Cloud\Spanner\Database}, and are as follows: * `Database::TYPE_BOOL`, `Database::TYPE_INT64`, @@ -87,7 +80,7 @@ class ArrayType * a struct is defined but the given type is not * `Database::TYPE_STRUCT`. */ - public function __construct($type) + public function __construct(int|string|StructType|null $type) { if ($type === Database::TYPE_STRUCT) { throw new \InvalidArgumentException( @@ -125,7 +118,7 @@ public function __construct($type) * @access private * @return int|string|null */ - public function type() + public function type(): int|string|null { return $this->type; } @@ -136,7 +129,7 @@ public function type() * @access private * @return StructType|null */ - public function structType() + public function structType(): StructType|null { return $this->structType; } diff --git a/Spanner/src/Backup.php b/Spanner/src/Backup.php index 8c791bb1a898..b9072e409431 100644 --- a/Spanner/src/Backup.php +++ b/Spanner/src/Backup.php @@ -17,16 +17,24 @@ namespace Google\Cloud\Spanner; +use Closure; use DateTimeInterface; +use Google\ApiCore\Options\CallOptions; use Google\ApiCore\ValidationException; -use Google\Cloud\Core\ArrayTrait; use Google\Cloud\Core\Exception\NotFoundException; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\Iterator\ItemIterator; +use Google\Cloud\Core\LongRunning\LongRunningClientConnection; use Google\Cloud\Core\LongRunning\LongRunningOperation; -use Google\Cloud\Core\LongRunning\LROTrait; +use Google\Cloud\Core\OptionsValidator; use Google\Cloud\Spanner\Admin\Database\V1\Backup\State; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Connection\ConnectionInterface; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\CopyBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\CreateBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\DeleteBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\GetBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\UpdateBackupRequest; +use Google\LongRunning\ListOperationsRequest; +use Google\LongRunning\Operation as OperationProto; /** * Represents a Cloud Spanner Backup. @@ -35,110 +43,48 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $backup = $spanner->instance('my-instance')->backup('my-backup'); * ``` - * - * @method resumeOperation() { - * Resume a long running operation - * - * Example: - * ``` - * $operation = $backup->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 = $backup->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 Backup { - use ArrayTrait; - use LROTrait; + use RequestTrait; const STATE_READY = State::READY; const STATE_CREATING = State::CREATING; - /** - * @var ConnectionInterface - * @internal - */ - private $connection; - - /** - * @var Instance - */ - private $instance; - - /** - * @var string - */ - private $projectId; - - /** - * @var string - */ - private $name; - - /** - * @var array - */ - private $info; + private array $info; /** * Create an object representing a Backup. * - * @param ConnectionInterface $connection The connection to the - * Cloud Spanner Admin API. This object is created by SpannerClient, - * and should not be instantiated outside of this client. + * @internal Backup is constructed by the {@see Instance} class. + * + * @param DatabaseAdminClient $databaseAdminClient The database admin client to make backup RPC + * calls. + * @param Serializer $serializer The serializer instance to encode/decode messages. * @param Instance $instance The instance in which the backup exists. - * @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 backup name or ID. - * @param array $info [optional] An array representing the backup resource. + * @param array $options [Optional] { + * Backup options. + + * @type array $backup The backup info. + * } */ public function __construct( - ConnectionInterface $connection, - Instance $instance, - LongRunningConnectionInterface $lroConnection, - array $lroCallables, - $projectId, - $name, - array $info = [] + private DatabaseAdminClient $databaseAdminClient, + private Serializer $serializer, + private Instance $instance, + private string $projectId, + private string $name, + array $options = [] ) { - $this->connection = $connection; - $this->instance = $instance; - $this->projectId = $projectId; $this->name = $this->fullyQualifiedBackupName($name); - $this->info = $info; - - $this->setLroProperties($lroConnection, $lroCallables, $this->name); + $this->info = $options['info'] ?? []; + $this->optionsValidator = new OptionsValidator($serializer); } /** @@ -161,32 +107,46 @@ public function __construct( * consistent copy of the database. If not present, it will be the same * as the create time of the backup. * } - * @return LongRunningOperation + * @return LongRunningOperation * @throws \InvalidArgumentException */ - public function create($database, DateTimeInterface $expireTime, array $options = []) - { - if (isset($options['versionTime'])) { - if (!($options['versionTime'] instanceof DateTimeInterface)) { - throw new \InvalidArgumentException( - 'Optional argument `versionTime` must be a DateTimeInterface, got ' . - (is_object($options['versionTime']) - ? get_class($options['versionTime']) - : gettype($options['versionTime'])) - ); - } - $options['versionTime'] = $options['versionTime']->format('Y-m-d\TH:i:s.u\Z'); - } - $operation = $this->connection->createBackup([ - 'instance' => $this->instance->name(), + public function create( + string $database, + DateTimeInterface $expireTime, + array $options = [] + ): LongRunningOperation { + $options += [ + 'parent' => $this->instance->name(), 'backupId' => DatabaseAdminClient::parseName($this->name)['backup'], 'backup' => [ 'database' => $this->instance->database($database)->name(), - 'expireTime' => $expireTime->format('Y-m-d\TH:i:s.u\Z'), + 'expireTime' => $this->formatTimeAsArray($expireTime), ], - ] + $options); + ]; + + if ($versionTime = $this->pluck('versionTime', $options, false)) { + if (!$versionTime instanceof DateTimeInterface) { + throw new \InvalidArgumentException( + 'Optional argument `versionTime` must be a DateTimeInterface' + ); + } + $options['backup']['versionTime'] = $this->formatTimeAsArray($versionTime); + } - return $this->resumeOperation($operation['name'], $operation); + /** + * @var CreateBackupRequest $createBackup + * @var array $callOptions + */ + [$createBackup, $callOptions] = $this->validateOptions( + $options, + new CreateBackupRequest(), + CallOptions::class + ); + + $operation = $this->databaseAdminClient->createBackup($createBackup, $callOptions + [ + 'resource-prefix' => $this->instance->name(), + ]); + return $this->operationFromOperationResponse($operation); } /** @@ -209,24 +169,36 @@ public function create($database, DateTimeInterface $expireTime, array $options * eligible to be automatically deleted by Cloud Spanner. * @param array $options [optional] { * Configuration Options. - * - * @type DateTimeInterface $versionTime The version time for the externally - * consistent copy of the database. If not present, it will be the same - * as the create time of the backup. * } - * @return LongRunningOperation + * @return LongRunningOperation * @throws \InvalidArgumentException */ - public function createCopy(Backup $newBackup, DateTimeInterface $expireTime, array $options = []) - { - $operation = $this->connection->copyBackup([ - 'instance' => $newBackup->instance->name(), + public function createCopy( + Backup $newBackup, + DateTimeInterface $expireTime, + array $options = [] + ): LongRunningOperation { + $options += [ + 'parent' => $newBackup->instance->name(), 'backupId' => DatabaseAdminClient::parseName($newBackup->name)['backup'], - 'sourceBackupId' => $this->fullyQualifiedBackupName($this->name), - 'expireTime' => $expireTime->format('Y-m-d\TH:i:s.u\Z') - ] + $options); + 'sourceBackup' => $this->fullyQualifiedBackupName($this->name), + 'expireTime' => $this->formatTimeAsArray($expireTime) + ]; + + /** + * @var CopyBackupRequest $copyBackup + * @var array $callOptions + */ + [$copyBackup, $callOptions] = $this->validateOptions( + $options, + new CopyBackupRequest(), + CallOptions::class + ); - return $this->resumeOperation($operation['name'], $operation); + $operation = $this->databaseAdminClient->copyBackup($copyBackup, $callOptions + [ + 'resource-prefix' => $this->instance->name(), + ]); + return $this->operationFromOperationResponse($operation); } /** @@ -240,9 +212,25 @@ public function createCopy(Backup $newBackup, DateTimeInterface $expireTime, arr * @param array $options [optional] Configuration options. * @return void */ - public function delete(array $options = []) + public function delete(array $options = []): void { - return $this->connection->deleteBackup(['name' => $this->name] + $options); + $options += [ + 'name' => $this->name + ]; + + /** + * @var DeleteBackupRequest $deleteBackup + * @var array $callOptions + */ + [$deleteBackup, $callOptions] = $this->validateOptions( + $options, + new DeleteBackupRequest(), + CallOptions::class, + ); + + $this->databaseAdminClient->deleteBackup($deleteBackup, $callOptions + [ + 'resource-prefix' => $this->name, + ]); } /** @@ -260,7 +248,7 @@ public function delete(array $options = []) * @param array $options [optional] Configuration options. * @return bool */ - public function exists(array $options = []) + public function exists(array $options = []): bool { try { $this->reload($options); @@ -282,7 +270,7 @@ public function exists(array $options = []) * @param array $options [optional] Configuration options. * @return array */ - public function info(array $options = []) + public function info(array $options = []): array { if (!$this->info) { $this->info = $this->reload($options); @@ -300,7 +288,7 @@ public function info(array $options = []) * * @return string */ - public function name() + public function name(): string { return $this->name; } @@ -316,11 +304,27 @@ public function name() * @param array $options [optional] Configuration options. * @return array */ - public function reload(array $options = []) + public function reload(array $options = []): array { - return $this->info = $this->connection->getBackup([ + $options += [ 'name' => $this->name - ] + $options); + ]; + + /** + * @var GetBackupRequest $getBackup + * @var array $callOptions + */ + [$getBackup, $callOptions] = $this->validateOptions( + $options, + new GetBackupRequest(), + CallOptions::class, + ); + + $response = $this->databaseAdminClient->getBackup($getBackup, $callOptions + [ + 'resource-prefix' => $this->name, + ]); + + return $this->info = $this->handleResponse($response); } /** @@ -344,7 +348,7 @@ public function reload(array $options = []) * @param array $options [optional] Configuration options. * @return int|null */ - public function state(array $options = []) + public function state(array $options = []): int|null { $info = $this->info($options); @@ -364,19 +368,109 @@ public function state(array $options = []) * @param DateTimeInterface $newTimestamp New expire time. * @param array $options [optional] Configuration options. * - * @return Backup + * @return array */ - public function updateExpireTime(DateTimeInterface $newTimestamp, array $options = []) + public function updateExpireTime(DateTimeInterface $newTimestamp, array $options = []): array { - return $this->info = $this->connection->updateBackup([ + $options += [ 'backup' => [ 'name' => $this->name(), - 'expireTime' => $newTimestamp->format('Y-m-d\TH:i:s.u\Z'), + 'expireTime' => $this->formatTimeAsArray($newTimestamp), ], 'updateMask' => [ 'paths' => ['expire_time'] ] - ] + $options); + ]; + + /** + * @var UpdateBackupRequest $updateBackup + * @var array $callOptions + */ + [$updateBackup, $callOptions] = $this->validateOptions( + $options, + new UpdateBackupRequest(), + CallOptions::class, + ); + + $response = $this->databaseAdminClient->updateBackup($updateBackup, $callOptions + [ + 'resource-prefix' => $this->name, + ]); + + return $this->info = $this->handleResponse($response); + } + + /** + * Resume a Long Running Operation + * + * Example: + * ``` + * $operation = $backup->resumeOperation($operationName); + * ``` + * + * @param string $operationName The Long Running Operation name. + * @return LongRunningOperation + */ + public function resumeOperation(string $operationName, array $options = []): LongRunningOperation + { + return new LongRunningOperation( + new LongRunningClientConnection($this->databaseAdminClient, $this->serializer), + $operationName, + [ + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata', + 'callable' => $this->backupResultFunction(), + ] + ], + $options + ); + } + + /** + * List long running operations. + * + * Example: + * ``` + * $operations = $backup->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 + */ + public function longRunningOperations(array $options = []): ItemIterator + { + /** + * @var ListOperationsRequest $listOperations + * @var array $callOptions + */ + [$listOperations, $callOptions] = $this->validateOptions( + $options, + new ListOperationsRequest(), + CallOptions::class, + ); + $listOperations->setName($this->name . '/operations'); + + return $this->buildLongRunningIterator( + [$this->databaseAdminClient->getOperationsClient(), 'listOperations'], + $listOperations, + $callOptions, + function (OperationProto $operation) { + return $this->resumeOperation( + $operation->getName(), + $this->handleResponse($operation) + ); + } + ); } /** @@ -384,7 +478,7 @@ public function updateExpireTime(DateTimeInterface $newTimestamp, array $options * * @return string */ - private function fullyQualifiedBackupName($name) + private function fullyQualifiedBackupName(string $name): string { $instance = DatabaseAdminClient::parseName($this->instance->name())['instance']; @@ -400,4 +494,12 @@ private function fullyQualifiedBackupName($name) } //@codeCoverageIgnoreEnd } + + private function backupResultFunction(): Closure + { + return function (array $backup) { + $name = DatabaseAdminClient::parseName($backup['name']); + return $this->instance->backup($name['backup'], $backup); + }; + } } diff --git a/Spanner/src/Batch/BatchClient.php b/Spanner/src/Batch/BatchClient.php index 1c72a60cf7aa..ee29725659a5 100644 --- a/Spanner/src/Batch/BatchClient.php +++ b/Spanner/src/Batch/BatchClient.php @@ -18,10 +18,10 @@ namespace Google\Cloud\Spanner\Batch; use Google\Cloud\Core\TimeTrait; -use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\TransactionConfigurationTrait; +use Google\Protobuf\Duration; /** * Provides Batch APIs used to read data from a Cloud Spanner database. @@ -36,7 +36,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $batch = $spanner->batch('instance-id', 'database-id'); * ``` * @@ -108,30 +108,15 @@ class BatchClient use TransactionConfigurationTrait; const PARTITION_TYPE_KEY = '__partitionTypeName'; - - /** - * @var Operation - */ - private $operation; - - /** - * @var string - */ - private $databaseName; - - /** - * @var string|null - */ - private $databaseRole; - - /** - * @var array - */ - private $allowedPartitionTypes = [ + private const ALLOWED_PARTITION_TYPES = [ QueryPartition::class, ReadPartition::class ]; + private Operation $operation; + private string $databaseName; + private string|null $databaseRole; + /** * @param Operation $operation A Cloud Spanner Operations wrapper. * @param string $databaseName The database name to which the batch client @@ -187,7 +172,7 @@ public function snapshot(array $options = []) $transactionOptions = $this->pluck('transactionOptions', $options); $transactionOptions['returnReadTimestamp'] = true; - $transactionOptions = $this->configureSnapshotOptions($transactionOptions); + $transactionOptions = $this->configureReadOnlyTransactionOptions($transactionOptions); if ($this->databaseRole !== null) { $sessionOptions['creator_role'] = $this->databaseRole; @@ -198,6 +183,7 @@ public function snapshot(array $options = []) $sessionOptions ); + /** @var BatchSnapshot */ return $this->operation->snapshot($session, [ 'className' => BatchSnapshot::class, 'transactionOptions' => $transactionOptions @@ -233,11 +219,20 @@ public function snapshotFromString($identifier) $session = $this->operation->session($data['sessionName']); - $readTime = $this->parseTimeString($data['readTimestamp']); - return $this->operation->createSnapshot($session, [ - 'id' => $data['transactionId'], - 'readTimestamp' => $data['readTimestamp'] - ], BatchSnapshot::class); + if ($data['readTimestamp']) { + if (!($data['readTimestamp'] instanceof Timestamp)) { + $time = $this->parseTimeString($data['readTimestamp']); + $data['readTimestamp'] = new Timestamp($time[0], $time[1]); + } + } + return new BatchSnapshot( + $this->operation, + $session, + [ + 'id' => $data['transactionId'], + 'readTimestamp' => $data['readTimestamp'] + ] + ); } /** @@ -262,7 +257,7 @@ public function partitionFromString($partition) } $class = $data[self::PARTITION_TYPE_KEY]; - if (!in_array($class, $this->allowedPartitionTypes)) { + if (!in_array($class, self::ALLOWED_PARTITION_TYPES)) { throw new \InvalidArgumentException('Invalid partition type.'); } diff --git a/Spanner/src/Batch/BatchSnapshot.php b/Spanner/src/Batch/BatchSnapshot.php index 301ff4ae8f44..a1399c81dce4 100644 --- a/Spanner/src/Batch/BatchSnapshot.php +++ b/Spanner/src/Batch/BatchSnapshot.php @@ -41,7 +41,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $batch = $spanner->batch('instance-id', 'database-id'); * $snapshot = $batch->snapshot(); * ``` @@ -92,7 +92,7 @@ public function __construct(Operation $operation, Session $session, array $optio * @param array $options [optional] Configuration Options * @return void */ - public function close(array $options = []) + public function close(array $options = []): void { $this->session->delete($options); } @@ -131,7 +131,7 @@ public function close(array $options = []) * } * @return ReadPartition[] */ - public function partitionRead($table, KeySet $keySet, array $columns, array $options = []) + public function partitionRead($table, KeySet $keySet, array $columns, array $options = []): array { return $this->operation->partitionRead( $this->session, @@ -194,7 +194,7 @@ public function partitionRead($table, KeySet $keySet, array $columns, array $opt * } * @return QueryPartition[] */ - public function partitionQuery($sql, array $options = []) + public function partitionQuery($sql, array $options = []): array { return $this->operation->partitionQuery( $this->session, @@ -218,11 +218,10 @@ public function partitionQuery($sql, array $options = []) * ``` * * @param PartitionInterface $partition The partition to read. - * @param array $options Configuration Options. * @return Result * @throws \BadMethodCallException If an invalid partition type is given. */ - public function executePartition(PartitionInterface $partition, array $options = []) + public function executePartition(PartitionInterface $partition): Result { if ($partition instanceof QueryPartition) { return $this->executeQuery($partition); @@ -243,7 +242,7 @@ public function executePartition(PartitionInterface $partition, array $options = * * @return string */ - public function serialize() + public function serialize(): string { return base64_encode(json_encode([ 'sessionName' => $this->session->name(), @@ -269,7 +268,7 @@ public function __toString() * @param QueryPartition $partition The partition. * @return Result */ - private function executeQuery(QueryPartition $partition) + private function executeQuery(QueryPartition $partition): Result { return $this->execute($partition->sql(), [ 'partitionToken' => $partition->token() @@ -282,7 +281,7 @@ private function executeQuery(QueryPartition $partition) * @param ReadPartition $partition The partition. * @return Result */ - private function executeRead(ReadPartition $partition) + private function executeRead(ReadPartition $partition): Result { return $this->read($partition->table(), $partition->keySet(), $partition->columns(), [ 'partitionToken' => $partition->token() diff --git a/Spanner/src/Batch/PartitionInterface.php b/Spanner/src/Batch/PartitionInterface.php index 956317020114..cc750d1d6489 100644 --- a/Spanner/src/Batch/PartitionInterface.php +++ b/Spanner/src/Batch/PartitionInterface.php @@ -30,11 +30,11 @@ public function __toString(); /** * @return string */ - public function serialize(); + public function serialize(): string; /** * @param array $data * @return PartitionInterface */ - public static function hydrate(array $data); + public static function hydrate(array $data): PartitionInterface; } diff --git a/Spanner/src/Batch/PartitionTrait.php b/Spanner/src/Batch/PartitionTrait.php index 3edb29a04f0d..15bb38cc6a56 100644 --- a/Spanner/src/Batch/PartitionTrait.php +++ b/Spanner/src/Batch/PartitionTrait.php @@ -22,15 +22,8 @@ */ trait PartitionTrait { - /** - * @var string - */ - private $token; - - /** - * @var array - */ - private $options; + private string $token; + private array $options; /** * Returns the partition token. @@ -42,7 +35,7 @@ trait PartitionTrait * * @return string */ - public function token() + public function token(): string { return $this->token; } @@ -57,7 +50,7 @@ public function token() * * @return array */ - public function options() + public function options(): array { return $this->options; } diff --git a/Spanner/src/Batch/QueryPartition.php b/Spanner/src/Batch/QueryPartition.php index 2b2589cde8ad..88ae306ab1c9 100644 --- a/Spanner/src/Batch/QueryPartition.php +++ b/Spanner/src/Batch/QueryPartition.php @@ -33,7 +33,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $batch = $spanner->batch('instance-id', 'database-id'); * $snapshot = $batch->snapshot(); * @@ -117,7 +117,7 @@ public function __construct($token, $sql, $options) * * @return string */ - public function sql() + public function sql(): string { return $this->sql; } @@ -132,7 +132,7 @@ public function sql() * * @return string */ - public function serialize() + public function serialize(): string { return base64_encode(json_encode(get_object_vars($this) + [ BatchClient::PARTITION_TYPE_KEY => static::class @@ -146,7 +146,7 @@ public function serialize() * @return QueryPartition * @access private */ - public static function hydrate(array $data) + public static function hydrate(array $data): self { return new self( $data['token'], diff --git a/Spanner/src/Batch/ReadPartition.php b/Spanner/src/Batch/ReadPartition.php index 22375864bb11..984b318971c5 100644 --- a/Spanner/src/Batch/ReadPartition.php +++ b/Spanner/src/Batch/ReadPartition.php @@ -36,7 +36,7 @@ * use Google\Cloud\Spanner\KeySet; * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $batch = $spanner->batch('instance-id', 'database-id'); * $snapshot = $batch->snapshot(); * @@ -118,7 +118,7 @@ public function __construct( * * @return string */ - public function table() + public function table(): string { return $this->table; } @@ -133,7 +133,7 @@ public function table() * * @return KeySet */ - public function keySet() + public function keySet(): KeySet { return $this->keySet; } @@ -148,7 +148,7 @@ public function keySet() * * @return array */ - public function columns() + public function columns(): array { return $this->columns; } @@ -163,7 +163,7 @@ public function columns() * * @return string */ - public function serialize() + public function serialize(): string { $vars = get_object_vars($this); $vars['keySet'] = $vars['keySet']->keySetObject(); @@ -180,7 +180,7 @@ public function serialize() * @return ReadPartition * @access private */ - public static function hydrate(array $data) + public static function hydrate(array $data): self { return new self( $data['token'], diff --git a/Spanner/src/BatchDmlResult.php b/Spanner/src/BatchDmlResult.php index e5b0a2f45b78..48dcda562370 100644 --- a/Spanner/src/BatchDmlResult.php +++ b/Spanner/src/BatchDmlResult.php @@ -25,7 +25,7 @@ * use Google\Cloud\Spanner\SpannerClient; * use Google\Cloud\Spanner\Transaction; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $database = $spanner->connect('my-instance', 'my-database'); * * $batchDmlResult = $database->runTransaction(function (Transaction $t) { @@ -47,20 +47,9 @@ */ class BatchDmlResult { - /** - * @var array - */ - private $data; - - /** - * @var array|null - */ - private $errorStatement; - - /** - * @var array|null - */ - private $rowCounts; + private array $data; + private array $rowCounts = []; + private array|null $errorStatement; /** * @param array $data The executeBatchDml response data. @@ -84,7 +73,7 @@ public function __construct(array $data, ?array $errorStatement = null) * * @return int[] */ - public function rowCounts() + public function rowCounts(): array { if (!$this->rowCounts) { foreach ($this->data['resultSets'] as $resultSet) { @@ -112,7 +101,7 @@ public function rowCounts() * * @return array|null */ - public function error() + public function error(): array|null { if ($this->errorStatement) { return [ diff --git a/Spanner/src/Bytes.php b/Spanner/src/Bytes.php index 0fc38020c079..3f6b00d50925 100644 --- a/Spanner/src/Bytes.php +++ b/Spanner/src/Bytes.php @@ -28,7 +28,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $bytes = $spanner->bytes('hello world'); * ``` @@ -43,12 +43,12 @@ class Bytes implements ValueInterface /** * @var string|resource|StreamInterface */ - private $value; + private mixed $value; /** * @param string|resource|StreamInterface $value The bytes value. */ - public function __construct($value) + public function __construct(mixed $value) { $this->value = Utils::streamFor($value); } @@ -63,7 +63,7 @@ public function __construct($value) * * @return StreamInterface */ - public function get() + public function get(): StreamInterface { return $this->value; } @@ -78,7 +78,7 @@ public function get() * * @return int */ - public function type() + public function type(): int { return Database::TYPE_BYTES; } @@ -93,7 +93,7 @@ public function type() * * @return string */ - public function formatAsString() + public function formatAsString(): string { return base64_encode((string) $this->value); } diff --git a/Spanner/src/CommitTimestamp.php b/Spanner/src/CommitTimestamp.php index d5aa05e59fe4..c2568861bc7f 100644 --- a/Spanner/src/CommitTimestamp.php +++ b/Spanner/src/CommitTimestamp.php @@ -41,7 +41,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $database = $spanner->connect('my-instance', 'my-database'); * * $database->insert('myTable', [ @@ -57,7 +57,7 @@ class CommitTimestamp implements ValueInterface /** * @access private */ - public function type() + public function type(): int { return Database::TYPE_TIMESTAMP; } @@ -65,7 +65,7 @@ public function type() /** * @access private */ - public function get() + public function get(): string { return self::SPECIAL_VALUE; } @@ -73,7 +73,7 @@ public function get() /** * @access private */ - public function formatAsString() + public function formatAsString(): string { return self::SPECIAL_VALUE; } diff --git a/Spanner/src/Connection/ConnectionInterface.php b/Spanner/src/Connection/ConnectionInterface.php deleted file mode 100644 index 24f417672720..000000000000 --- a/Spanner/src/Connection/ConnectionInterface.php +++ /dev/null @@ -1,291 +0,0 @@ - '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' => UpdateDatabaseDdlMetadata::class - ], [ - 'method' => 'createDatabase', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata', - 'message' => CreateDatabaseMetadata::class - ], [ - 'method' => 'createInstanceConfig', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata', - 'message' => CreateInstanceConfigMetadata::class - ], [ - 'method' => 'updateInstanceConfig', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata', - 'message' => UpdateInstanceConfigMetadata::class - ], [ - 'method' => 'createInstance', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata', - 'message' => CreateInstanceMetadata::class - ], [ - 'method' => 'updateInstance', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', - 'message' => UpdateInstanceMetadata::class - ], [ - 'method' => 'createBackup', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata', - 'message' => CreateBackupMetadata::class - ], [ - 'method' => 'copyBackup', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CopyBackupMetadata', - 'message' => CopyBackupMetadata::class - ], [ - 'method' => 'restoreDatabase', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata', - 'message' => RestoreDatabaseMetadata::class - ], [ - 'method' => 'restoreDatabase', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.OptimizeRestoredDatabaseMetadata', - 'message' => OptimizeRestoredDatabaseMetadata::class - ], [ - 'method' => 'updateDatabaseDdl', - 'typeUrl' => 'type.googleapis.com/google.protobuf.Empty', - 'message' => GPBEmpty::class - ], [ - 'method' => 'createDatabase', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.Database', - 'message' => Database::class - ], [ - 'method' => 'createInstanceConfig', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.InstanceConfig', - 'message' => InstanceConfig::class - ], [ - 'method' => 'updateInstanceConfig', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.InstanceConfig', - 'message' => InstanceConfig::class - ], [ - 'method' => 'createInstance', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.Instance', - 'message' => Instance::class - ], [ - 'method' => 'updateInstance', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.Instance', - 'message' => Instance::class - ], [ - 'method' => 'createBackup', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.Backup', - 'message' => Backup::class - ], [ - 'method' => 'restoreDatabase', - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.Database', - 'message' => Database::class - ] - ]; - - /** - * @var CredentialsWrapper - */ - private $credentialsWrapper; - - /** - * @var bool - */ - private $larEnabled; - - /** - * @param array $config [optional] - */ - public function __construct(array $config = []) - { - //@codeCoverageIgnoreStart - $this->serializer = new Serializer([], [ - 'google.protobuf.Value' => function ($v) { - return $this->flattenValue($v); - }, - 'google.protobuf.ListValue' => function ($v) { - return $this->flattenListValue($v); - }, - 'google.protobuf.Struct' => function ($v) { - return $this->flattenStruct($v); - }, - 'google.protobuf.Timestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - } - ], [], [], [ - // A custom encoder that short-circuits the encodeMessage in Serializer class, - // but only if the argument is of the type PartialResultSet. - PartialResultSet::class => function ($msg) { - $data = json_decode($msg->serializeToJsonString(), true); - - // We only override metadata fields, if it actually exists in the response. - // This is specially important for large data sets which is received in chunks. - // Metadata is only received in the first 'chunk' and we don't want to set empty metadata fields - // when metadata was not returned from the server. - if (isset($data['metadata'])) { - // The transaction id is serialized as a base64 encoded string in $data. So, we - // add a step to get the transaction id using a getter instead of the serialized value. - // The null-safe operator is used to handle edge cases where the relevant fields are not present. - $data['metadata']['transaction']['id'] = (string) $msg?->getMetadata()?->getTransaction()?->getId(); - - // Helps convert metadata enum values from string types to their respective code/annotation - // pairs. Ex: INT64 is converted to {code: 2, typeAnnotation: 0}. - $fields = $msg->getMetadata()?->getRowType()?->getFields(); - $data['metadata']['rowType']['fields'] = $this->getFieldDataFromRepeatedFields($fields); - } - - // These fields in stats should be an int - if (isset($data['stats']['rowCountLowerBound'])) { - $data['stats']['rowCountLowerBound'] = (int) $data['stats']['rowCountLowerBound']; - } - if (isset($data['stats']['rowCountExact'])) { - $data['stats']['rowCountExact'] = (int) $data['stats']['rowCountExact']; - } - - return $data; - } - ]); - //@codeCoverageIgnoreEnd - - $config['serializer'] = $this->serializer; - $this->setRequestWrapper(new GrpcRequestWrapper($config)); - $grpcConfig = $this->getGaxConfig( - ManualSpannerClient::VERSION, - isset($config['authHttpHandler']) - ? $config['authHttpHandler'] - : null, - $config['universeDomain'] ?? null - ); - - $config += [ - 'emulatorHost' => null, - 'queryOptions' => [], - // If the user has not supplied a universe domain, use the environment variable if set. - // Otherwise, use the default ("googleapis.com"). - 'universeDomain' => getenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN') - ?: GetUniverseDomainInterface::DEFAULT_UNIVERSE_DOMAIN, - ]; - if ((bool) $config['emulatorHost']) { - $grpcConfig = array_merge( - $grpcConfig, - $this->emulatorGapicConfig($config['emulatorHost']) - ); - } elseif (isset($config['apiEndpoint'])) { - $grpcConfig['apiEndpoint'] = $config['apiEndpoint']; - } - - // configure the universe domain if set - if (isset($config['universeDomain'])) { - $grpcConfig['universeDomain'] = $config['universeDomain']; - } - - $this->credentialsWrapper = $grpcConfig['credentials']; - - $this->defaultQueryOptions = $config['queryOptions']; - - $this->spannerClient = $config['gapicSpannerClient'] - ?? $this->constructGapic(SpannerClient::class, $grpcConfig); - - //@codeCoverageIgnoreStart - if (isset($config['gapicSpannerInstanceAdminClient'])) { - $this->instanceAdminClient = $config['gapicSpannerInstanceAdminClient']; - } - - if (isset($config['gapicSpannerDatabaseAdminClient'])) { - $this->databaseAdminClient = $config['gapicSpannerDatabaseAdminClient']; - } - //@codeCoverageIgnoreEnd - - $this->grpcConfig = $grpcConfig; - $this->larEnabled = $this->pluck('routeToLeader', $config, false) ?? true; - } - - /** - * @param array $args - */ - public function listInstanceConfigs(array $args) - { - $projectName = $this->pluck('projectName', $args); - return $this->send([$this->getInstanceAdminClient(), 'listInstanceConfigs'], [ - $projectName, - $this->addResourcePrefixHeader($args, $projectName) - ]); - } - - /** - * @param array $args - */ - public function getInstanceConfig(array $args) - { - $projectName = $this->pluck('projectName', $args); - return $this->send([$this->getInstanceAdminClient(), 'getInstanceConfig'], [ - $this->pluck('name', $args), - $this->addResourcePrefixHeader($args, $projectName) - ]); - } - - /** - * @param array $args - */ - public function createInstanceConfig(array $args) - { - $instanceConfigName = $args['name']; - - $instanceConfig = $this->instanceConfigObject($args, true); - $res = $this->send([$this->getInstanceAdminClient(), 'createInstanceConfig'], [ - $this->pluck('projectName', $args), - $this->pluck('instanceConfigId', $args), - $instanceConfig, - $this->addResourcePrefixHeader($args, $instanceConfigName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function updateInstanceConfig(array $args) - { - $instanceConfigName = $args['name']; - - $instanceConfigArray = $this->instanceConfigArray($args); - - $fieldMask = $this->fieldMask($instanceConfigArray); - - $instanceConfigObject = $this->serializer->decodeMessage(new InstanceConfig(), $instanceConfigArray); - - $res = $this->send([$this->getInstanceAdminClient(), 'updateInstanceConfig'], [ - $instanceConfigObject, - $fieldMask, - $this->addResourcePrefixHeader($args, $instanceConfigName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function deleteInstanceConfig(array $args) - { - $instanceConfigName = $this->pluck('name', $args); - return $this->send([$this->getInstanceAdminClient(), 'deleteInstanceConfig'], [ - $instanceConfigName, - $this->addResourcePrefixHeader($args, $instanceConfigName) - ]); - } - - /** - * @param array $args - */ - public function listInstanceConfigOperations(array $args) - { - $projectName = $this->pluck('projectName', $args); - $result = $this->send([$this->getInstanceAdminClient(), 'listInstanceConfigOperations'], [ - $projectName, - $this->addResourcePrefixHeader($args, $projectName) - ]); - foreach ($result['operations'] as $index => $operation) { - $result['operations'][$index] = $this->deserializeOperationArray($operation); - } - return $result; - } - - /** - * @param array $args - */ - public function listInstances(array $args) - { - $projectName = $this->pluck('projectName', $args); - return $this->send([$this->getInstanceAdminClient(), 'listInstances'], [ - $projectName, - $this->addResourcePrefixHeader($args, $projectName) - ]); - } - - /** - * @param array $args - */ - public function getInstance(array $args) - { - $projectName = $this->pluck('projectName', $args); - - if (isset($args['fieldMask'])) { - $mask = []; - if (is_array($args['fieldMask'])) { - foreach (array_values($args['fieldMask']) as $field) { - $mask[] = Serializer::toSnakeCase($field); - } - } else { - $mask[] = Serializer::toSnakeCase($args['fieldMask']); - } - $fieldMask = $this->serializer->decodeMessage(new FieldMask(), ['paths' => $mask]); - $args['fieldMask'] = $fieldMask; - } - - return $this->send([$this->getInstanceAdminClient(), 'getInstance'], [ - $this->pluck('name', $args), - $this->addResourcePrefixHeader($args, $projectName) - ]); - } - - /** - * @param array $args - */ - public function createInstance(array $args) - { - $instanceName = $args['name']; - - $instance = $this->instanceObject($args, true); - $res = $this->send([$this->getInstanceAdminClient(), 'createInstance'], [ - $this->pluck('projectName', $args), - $this->pluck('instanceId', $args), - $instance, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function updateInstance(array $args) - { - $instanceName = $args['name']; - - $instanceArray = $this->instanceArray($args); - - $fieldMask = $this->fieldMask($instanceArray); - - $instanceObject = $this->serializer->decodeMessage(new Instance(), $instanceArray); - - $res = $this->send([$this->getInstanceAdminClient(), 'updateInstance'], [ - $instanceObject, - $fieldMask, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function deleteInstance(array $args) - { - $instanceName = $this->pluck('name', $args); - return $this->send([$this->getInstanceAdminClient(), 'deleteInstance'], [ - $instanceName, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - } - - /** - * @param array $args - */ - public function getInstanceIamPolicy(array $args) - { - $resource = $this->pluck('resource', $args); - return $this->send([$this->getInstanceAdminClient(), 'getIamPolicy'], [ - $resource, - $this->addResourcePrefixHeader($args, $resource) - ]); - } - - /** - * @param array $args - */ - public function setInstanceIamPolicy(array $args) - { - $resource = $this->pluck('resource', $args); - return $this->send([$this->getInstanceAdminClient(), 'setIamPolicy'], [ - $resource, - $this->pluck('policy', $args), - $this->addResourcePrefixHeader($args, $resource) - ]); - } - - /** - * @param array $args - */ - public function testInstanceIamPermissions(array $args) - { - $resource = $this->pluck('resource', $args); - return $this->send([$this->getInstanceAdminClient(), 'testIamPermissions'], [ - $resource, - $this->pluck('permissions', $args), - $this->addResourcePrefixHeader($args, $resource) - ]); - } - - /** - * @param array $args - */ - public function listBackups(array $args) - { - $instanceName = $this->pluck('instance', $args); - return $this->send([$this->getDatabaseAdminClient(), 'listBackups'], [ - $instanceName, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - } - - /** - * @param array $args - */ - public function listBackupOperations(array $args) - { - $instanceName = $this->pluck('instance', $args); - $result = $this->send([$this->getDatabaseAdminClient(), 'listBackupOperations'], [ - $instanceName, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - foreach ($result['operations'] as $index => $operation) { - $result['operations'][$index] = $this->deserializeOperationArray($operation); - } - return $result; - } - - /** - * @param array $args - */ - public function listDatabaseOperations(array $args) - { - $instanceName = $this->pluck('instance', $args); - $result = $this->send([$this->getDatabaseAdminClient(), 'listDatabaseOperations'], [ - $instanceName, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - foreach ($result['operations'] as $index => $operation) { - $result['operations'][$index] = $this->deserializeOperationArray($operation); - } - return $result; - } - - /** - * @param array $args - */ - public function restoreDatabase(array $args) - { - $instanceName = $this->pluck('instance', $args); - if (isset($args['encryptionConfig'])) { - $args['encryptionConfig'] = $this->serializer->decodeMessage( - new RestoreDatabaseEncryptionConfig(), - $this->pluck('encryptionConfig', $args) - ); - } - $res = $this->send([$this->getDatabaseAdminClient(), 'restoreDatabase'], [ - $instanceName, - $this->pluck('databaseId', $args), - $this->addResourcePrefixHeader($args, $instanceName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function updateBackup(array $args) - { - $backup = $this->pluck('backup', $args); - $backup['expireTime'] = $this->formatTimestampForApi($this->pluck('expireTime', $backup)); - $backupInfo = $this->serializer->decodeMessage(new Backup(), $backup); - - $backupName = $backupInfo->getName(); - $updateMask = $this->serializer->decodeMessage(new FieldMask(), $this->pluck('updateMask', $args)); - return $this->send([$this->getDatabaseAdminClient(), 'updateBackup'], [ - $backupInfo, - $updateMask, - $this->addResourcePrefixHeader($args, $backupName) - ]); - } - - /** - * @param array $args - */ - public function createBackup(array $args) - { - $backup = $this->pluck('backup', $args); - $backup['expireTime'] = $this->formatTimestampForApi($this->pluck('expireTime', $backup)); - if (isset($args['versionTime'])) { - $backup['versionTime'] = $this->formatTimestampForApi($this->pluck('versionTime', $args)); - } - $backupInfo = $this->serializer->decodeMessage(new Backup(), $backup); - - $instanceName = $this->pluck('instance', $args); - if (isset($args['encryptionConfig'])) { - $args['encryptionConfig'] = $this->serializer->decodeMessage( - new CreateBackupEncryptionConfig(), - $this->pluck('encryptionConfig', $args) - ); - } - $res = $this->send([$this->getDatabaseAdminClient(), 'createBackup'], [ - $instanceName, - $this->pluck('backupId', $args), - $backupInfo, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function copyBackup(array $args) - { - $instanceName = $this->pluck('instance', $args); - $expireTime = new Timestamp( - $this->formatTimestampForApi($this->pluck('expireTime', $args)) - ); - - $res = $this->send([$this->getDatabaseAdminClient(), 'copyBackup'], [ - $instanceName, - $this->pluck('backupId', $args), - $this->pluck('sourceBackupId', $args), - $expireTime, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function deleteBackup(array $args) - { - $backupName = $this->pluck('name', $args); - return $this->send([$this->getDatabaseAdminClient(), 'deleteBackup'], [ - $backupName, - $this->addResourcePrefixHeader($args, $backupName) - ]); - } - - /** - * @param array $args - */ - public function getBackup(array $args) - { - $backupName = $this->pluck('name', $args); - return $this->send([$this->getDatabaseAdminClient(), 'getBackup'], [ - $backupName, - $this->addResourcePrefixHeader($args, $backupName) - ]); - } - - /** - * @param array $args - */ - public function listDatabases(array $args) - { - $instanceName = $this->pluck('instance', $args); - return $this->send([$this->getDatabaseAdminClient(), 'listDatabases'], [ - $instanceName, - $this->addResourcePrefixHeader($args, $instanceName) - ]); - } - - /** - * @param array $args - */ - public function createDatabase(array $args) - { - $instanceName = $this->pluck('instance', $args); - if (isset($args['encryptionConfig'])) { - $args['encryptionConfig'] = $this->serializer->decodeMessage( - new EncryptionConfig(), - $this->pluck('encryptionConfig', $args) - ); - } - $res = $this->send([$this->getDatabaseAdminClient(), 'createDatabase'], [ - $instanceName, - $this->pluck('createStatement', $args), - $this->addResourcePrefixHeader($args, $instanceName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function updateDatabase(array $args) - { - $databaseInfo = $this->serializer->decodeMessage(new Database(), $this->pluck('database', $args)); - $databaseName = $databaseInfo->getName(); - $updateMask = $this->serializer->decodeMessage(new FieldMask(), $this->pluck('updateMask', $args)); - return $this->send([$this->getDatabaseAdminClient(), 'updateDatabase'], [ - $databaseInfo, - $updateMask, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function updateDatabaseDdl(array $args) - { - $databaseName = $this->pluck('name', $args); - $res = $this->send([$this->getDatabaseAdminClient(), 'updateDatabaseDdl'], [ - $databaseName, - $this->pluck('statements', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - - return $this->operationToArray($res, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function dropDatabase(array $args) - { - $databaseName = $this->pluck('name', $args); - return $this->send([$this->getDatabaseAdminClient(), 'dropDatabase'], [ - $databaseName, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function getDatabase(array $args) - { - $databaseName = $this->pluck('name', $args); - return $this->send([$this->getDatabaseAdminClient(), 'getDatabase'], [ - $databaseName, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function getDatabaseDdl(array $args) - { - $databaseName = $this->pluck('name', $args); - return $this->send([$this->getDatabaseAdminClient(), 'getDatabaseDdl'], [ - $databaseName, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function getDatabaseIamPolicy(array $args) - { - $databaseName = $this->pluck('resource', $args); - return $this->send([$this->getDatabaseAdminClient(), 'getIamPolicy'], [ - $databaseName, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function setDatabaseIamPolicy(array $args) - { - $databaseName = $this->pluck('resource', $args); - return $this->send([$this->getDatabaseAdminClient(), 'setIamPolicy'], [ - $databaseName, - $this->pluck('policy', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function testDatabaseIamPermissions(array $args) - { - $databaseName = $this->pluck('resource', $args); - return $this->send([$this->getDatabaseAdminClient(), 'testIamPermissions'], [ - $databaseName, - $this->pluck('permissions', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function createSession(array $args) - { - $databaseName = $this->pluck('database', $args); - - $session = $this->pluck('session', $args, false); - if ($session) { - $args['session'] = $this->serializer->decodeMessage( - new Session(), - array_filter( - $session, - function ($value) { - return !is_null($value); - } - ) - ); - } - - $args = $this->addLarHeader($args, $this->larEnabled); - return $this->send([$this->spannerClient, 'createSession'], [ - $databaseName, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * Note: This should be removed once GAPIC exposes the ability to execute - * concurrent requests. - * - * @access private - * @experimental - * @param array $args - * @return PromiseInterface - */ - public function createSessionAsync(array $args) - { - $databaseName = $this->pluck('database', $args); - $opts = $this->addResourcePrefixHeader([], $databaseName); - $opts = $this->addLarHeader($opts, $this->larEnabled); - $opts['credentialsWrapper'] = $this->credentialsWrapper; - $transport = $this->spannerClient->getTransport(); - - $request = new CreateSessionRequest([ - 'database' => $databaseName - ]); - - $session = $this->pluck('session', $args, false); - - if ($session) { - $sessionMessage = new Session($session); - $request->setSession($sessionMessage); - } - - return $transport->startUnaryCall( - new Call( - 'google.spanner.v1.Spanner/CreateSession', - Session::class, - $request - ), - $opts - ); - } - - /** - * @param array $args - */ - public function batchCreateSessions(array $args) - { - $args['sessionTemplate'] = $this->serializer->decodeMessage( - new Session(), - $this->pluck('sessionTemplate', $args) - ); - - $databaseName = $this->pluck('database', $args); - $args = $this->addLarHeader($args, $this->larEnabled); - return $this->send([$this->spannerClient, 'batchCreateSessions'], [ - $databaseName, - $this->pluck('sessionCount', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function getSession(array $args) - { - $databaseName = $this->pluck('database', $args); - $args = $this->addLarHeader($args, $this->larEnabled); - return $this->send([$this->spannerClient, 'getSession'], [ - $this->pluck('name', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function deleteSession(array $args) - { - $databaseName = $this->pluck('database', $args); - return $this->send([$this->spannerClient, 'deleteSession'], [ - $this->pluck('name', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * Note: This should be removed once GAPIC exposes the ability to execute - * concurrent requests. - * - * @access private - * @param array $args - * @return PromiseInterface - * @experimental - */ - public function deleteSessionAsync(array $args) - { - $databaseName = $this->pluck('database', $args); - $request = new DeleteSessionRequest(); - $request->setName($this->pluck('name', $args)); - - $transport = $this->spannerClient->getTransport(); - $opts = $this->addResourcePrefixHeader([], $databaseName); - $opts['credentialsWrapper'] = $this->credentialsWrapper; - - return $transport->startUnaryCall( - new Call( - 'google.spanner.v1.Spanner/DeleteSession', - GPBEmpty::class, - $request - ), - $opts - ); - } - - /** - * @param array $args - * @return \Generator - */ - public function executeStreamingSql(array $args) - { - $args = $this->formatSqlParams($args); - $args['transaction'] = $this->createTransactionSelector($args); - - $databaseName = $this->pluck('database', $args); - $queryOptions = $this->pluck('queryOptions', $args, false) ?: []; - - // Query options precedence is query-level, then environment-level, then client-level. - $envQueryOptimizerVersion = getenv('SPANNER_OPTIMIZER_VERSION'); - $envQueryOptimizerStatisticsPackage = getenv('SPANNER_OPTIMIZER_STATISTICS_PACKAGE'); - if (!empty($envQueryOptimizerVersion)) { - $queryOptions += ['optimizerVersion' => $envQueryOptimizerVersion]; - } - if (!empty($envQueryOptimizerStatisticsPackage)) { - $queryOptions += ['optimizerStatisticsPackage' => $envQueryOptimizerStatisticsPackage]; - } - $queryOptions += $this->defaultQueryOptions; - $this->setDirectedReadOptions($args); - - if ($queryOptions) { - $args['queryOptions'] = $this->serializer->decodeMessage( - new QueryOptions(), - $queryOptions - ); - } - - $requestOptions = $this->pluck('requestOptions', $args, false) ?: []; - if ($requestOptions) { - $args['requestOptions'] = $this->serializer->decodeMessage( - new RequestOptions(), - $requestOptions - ); - } - - $args = $this->conditionallyUnsetLarHeader($args, $this->larEnabled); - return $this->send([$this->spannerClient, 'executeStreamingSql'], [ - $this->pluck('session', $args), - $this->pluck('sql', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - * @return \Generator - */ - public function streamingRead(array $args) - { - $keySet = $this->pluck('keySet', $args); - $keySet = $this->serializer->decodeMessage(new KeySet(), $this->formatKeySet($keySet)); - - $requestOptions = $this->pluck('requestOptions', $args, false) ?: []; - if ($requestOptions) { - $args['requestOptions'] = $this->serializer->decodeMessage( - new RequestOptions(), - $requestOptions - ); - } - $this->setDirectedReadOptions($args); - $args['transaction'] = $this->createTransactionSelector($args); - - $databaseName = $this->pluck('database', $args); - $args = $this->conditionallyUnsetLarHeader($args, $this->larEnabled); - return $this->send([$this->spannerClient, 'streamingRead'], [ - $this->pluck('session', $args), - $this->pluck('table', $args), - $this->pluck('columns', $args), - $keySet, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function executeBatchDml(array $args) - { - $databaseName = $this->pluck('database', $args); - $args['transaction'] = $this->createTransactionSelector($args); - $args = $this->addLarHeader($args, $this->larEnabled); - - $statements = []; - foreach ($this->pluck('statements', $args) as $statement) { - $statement = $this->formatSqlParams($statement); - $statements[] = $this->serializer->decodeMessage(new Statement(), $statement); - } - - $requestOptions = $this->pluck('requestOptions', $args, false) ?: []; - if ($requestOptions) { - $args['requestOptions'] = $this->serializer->decodeMessage( - new RequestOptions(), - $requestOptions - ); - } - - return $this->send([$this->spannerClient, 'executeBatchDml'], [ - $this->pluck('session', $args), - $this->pluck('transaction', $args), - $statements, - $this->pluck('seqno', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function beginTransaction(array $args) - { - $options = new TransactionOptions(); - $transactionOptions = $this->formatTransactionOptions($this->pluck('transactionOptions', $args)); - if (isset($transactionOptions['readOnly'])) { - $readOnly = $this->serializer->decodeMessage( - new PBReadOnly(), - $transactionOptions['readOnly'] - ); - $options->setReadOnly($readOnly); - } elseif (isset($transactionOptions['readWrite'])) { - $readWrite = new ReadWrite(); - $options->setReadWrite($readWrite); - $args = $this->addLarHeader($args, $this->larEnabled); - - if (isset($transactionOptions['readWrite']['readLockMode'])) { - // Nested option `readLockMode` inside `readWrite` transactions - $readLockModeOption = $transactionOptions['readWrite']['readLockMode']; - $options->getReadWrite()->setReadLockMode($readLockModeOption); - } - } elseif (isset($transactionOptions['partitionedDml'])) { - $pdml = new PartitionedDml(); - $options->setPartitionedDml($pdml); - $args = $this->addLarHeader($args, $this->larEnabled); - } - - if (isset($transactionOptions['isolationLevel'])) { - $options->setIsolationLevel($transactionOptions['isolationLevel']); - } - - // NOTE: if set for read-only actions, will throw exception - if (isset($transactionOptions['excludeTxnFromChangeStreams'])) { - $options->setExcludeTxnFromChangeStreams( - $transactionOptions['excludeTxnFromChangeStreams'] - ); - } - - $requestOptions = $this->pluck('requestOptions', $args, false) ?: []; - if ($requestOptions) { - $args['requestOptions'] = $this->serializer->decodeMessage( - new RequestOptions(), - $requestOptions - ); - } - - $databaseName = $this->pluck('database', $args); - return $this->send([$this->spannerClient, 'beginTransaction'], [ - $this->pluck('session', $args), - $options, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function commit(array $args) - { - $inputMutations = $this->pluck('mutations', $args); - - $mutations = $this->parseMutations($inputMutations); - - if (isset($args['singleUseTransaction'])) { - $readWrite = $this->serializer->decodeMessage( - new ReadWrite(), - [] - ); - - $options = new TransactionOptions(); - $options->setReadWrite($readWrite); - $args['singleUseTransaction'] = $options; - } - - $requestOptions = $this->pluck('requestOptions', $args, false) ?: []; - if ($requestOptions) { - $args['requestOptions'] = $this->serializer->decodeMessage( - new RequestOptions(), - $requestOptions - ); - } - - $databaseName = $this->pluck('database', $args); - $args = $this->addLarHeader($args, $this->larEnabled); - return $this->send([$this->spannerClient, 'commit'], [ - $this->pluck('session', $args), - $mutations, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - * @return \Generator - */ - public function batchWrite(array $args) - { - $databaseName = $this->pluck('database', $args); - $mutationGroups = $this->pluck('mutationGroups', $args); - $requestOptions = $this->pluck('requestOptions', $args, false) ?: []; - - array_walk( - $mutationGroups, - fn (&$x) => $x['mutations'] = $this->parseMutations($x['mutations']) - ); - - $mutationGroups = array_map( - fn ($x) => $this->serializer->decodeMessage(new MutationGroupProto(), $x), - $mutationGroups - ); - - if ($requestOptions) { - $args['requestOptions'] = $this->serializer->decodeMessage( - new RequestOptions(), - $requestOptions - ); - } - - return $this->send([$this->spannerClient, 'batchWrite'], [ - $this->pluck('session', $args), - $mutationGroups, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function rollback(array $args) - { - $databaseName = $this->pluck('database', $args); - $args = $this->addLarHeader($args, $this->larEnabled); - return $this->send([$this->spannerClient, 'rollback'], [ - $this->pluck('session', $args), - $this->pluck('transactionId', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function partitionQuery(array $args) - { - $args = $this->formatSqlParams($args); - $args['transaction'] = $this->createTransactionSelector($args); - $args = $this->addLarHeader($args, $this->larEnabled); - - $args['partitionOptions'] = $this->serializer->decodeMessage( - new PartitionOptions(), - $this->pluck('partitionOptions', $args, false) ?: [] - ); - - $databaseName = $this->pluck('database', $args); - return $this->send([$this->spannerClient, 'partitionQuery'], [ - $this->pluck('session', $args), - $this->pluck('sql', $args), - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function partitionRead(array $args) - { - $keySet = $this->pluck('keySet', $args); - $keySet = $this->serializer->decodeMessage(new KeySet(), $this->formatKeySet($keySet)); - - $args['transaction'] = $this->createTransactionSelector($args); - $args = $this->addLarHeader($args, $this->larEnabled); - - $args['partitionOptions'] = $this->serializer->decodeMessage( - new PartitionOptions(), - $this->pluck('partitionOptions', $args, false) ?: [] - ); - - $databaseName = $this->pluck('database', $args); - return $this->send([$this->spannerClient, 'partitionRead'], [ - $this->pluck('session', $args), - $this->pluck('table', $args), - $keySet, - $this->addResourcePrefixHeader($args, $databaseName) - ]); - } - - /** - * @param array $args - */ - public function getOperation(array $args) - { - $name = $this->pluck('name', $args); - - $operation = $this->getOperationByName($this->getDatabaseAdminClient(), $name); - - return $this->operationToArray($operation, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function cancelOperation(array $args) - { - $name = $this->pluck('name', $args); - $method = $this->pluck('method', $args, false); - - $operation = $this->getOperationByName($this->getDatabaseAdminClient(), $name, $method); - $operation->cancel(); - - return $this->operationToArray($operation, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function deleteOperation(array $args) - { - $name = $this->pluck('name', $args); - $method = $this->pluck('method', $args, false); - - $operation = $this->getOperationByName($this->getDatabaseAdminClient(), $name, $method); - $operation->delete(); - - return $this->operationToArray($operation, $this->serializer, $this->lroResponseMappers); - } - - /** - * @param array $args - */ - public function listOperations(array $args) - { - $name = $this->pluck('name', $args, false) ?: ''; - $filter = $this->pluck('filter', $args, false) ?: ''; - - $client = $this->getDatabaseAdminClient()->getOperationsClient(); - - return $this->send([$client, 'listOperations'], [ - $name, - $filter, - $args - ]); - } - - /** - * @param array $args - * @return array - */ - private function formatSqlParams(array $args) - { - $params = $this->pluck('params', $args); - if ($params) { - $modifiedParams = []; - foreach ($params as $key => $param) { - $modifiedParams[$key] = $this->fieldValue($param); - } - $args['params'] = new Struct(); - $args['params']->setFields($modifiedParams); - } - - if (isset($args['paramTypes']) && is_array($args['paramTypes'])) { - foreach ($args['paramTypes'] as $key => $param) { - $args['paramTypes'][$key] = $this->serializer->decodeMessage(new Type(), $param); - } - } - - return $args; - } - - /** - * @param array $keySet - * @return array Formatted keyset - */ - private function formatKeySet(array $keySet) - { - $keys = $this->pluck('keys', $keySet, false); - if ($keys) { - $keySet['keys'] = []; - - foreach ($keys as $key) { - $keySet['keys'][] = $this->formatListForApi((array) $key); - } - } - - 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 TransactionSelector - */ - private function createTransactionSelector(array &$args) - { - $selector = new TransactionSelector(); - if (isset($args['transaction'])) { - $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 = $this->serializer->decodeMessage($selector, $transaction); - } elseif (isset($args['transactionId'])) { - $selector = $this->serializer->decodeMessage($selector, ['id' => $this->pluck('transactionId', $args)]); - } - - return $selector; - } - - /** - * Converts a PHP array to an InstanceConfig proto message. - * - * @param array $args - * @param bool $required - * @return InstanceConfig - */ - private function instanceConfigObject(array &$args, $required = false) - { - return $this->serializer->decodeMessage( - new InstanceConfig(), - $this->instanceConfigArray($args, $required) - ); - } - - /** - * Creates a PHP array with only the fields that are relevant for an InstanceConfig. - * - * @param array $args - * @param bool $required - * @return array - */ - private function instanceConfigArray(array &$args, $required = false) - { - $argsCopy = $args; - return array_intersect_key([ - 'name' => $this->pluck('name', $args, $required), - 'baseConfig' => $this->pluck('baseConfig', $args, $required), - 'displayName' => $this->pluck('displayName', $args, $required), - 'configType' => $this->pluck('configType', $args, $required), - 'replicas' => $this->pluck('replicas', $args, $required), - 'optionalReplicas' => $this->pluck('optionalReplicas', $args, $required), - 'leaderOptions' => $this->pluck('leaderOptions', $args, $required), - 'reconciling' => $this->pluck('reconciling', $args, $required), - 'state' => $this->pluck('state', $args, $required), - 'labels' => $this->pluck('labels', $args, $required), - ], $argsCopy); - } - - /** - * @param array $args - * @param bool $required - * @return Instance - */ - private function instanceObject(array &$args, $required = false) - { - return $this->serializer->decodeMessage( - new Instance(), - $this->instanceArray($args, $required) - ); - } - - /** - * @param array $args - * @param bool $required - * @return array - */ - private function instanceArray(array &$args, $required = false) - { - $argsCopy = $args; - if (isset($args['nodeCount'])) { - return array_intersect_key([ - '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->pluck('labels', $args, $required), - ], $argsCopy); - } - return array_intersect_key([ - 'name' => $this->pluck('name', $args, $required), - 'config' => $this->pluck('config', $args, $required), - 'displayName' => $this->pluck('displayName', $args, $required), - 'processingUnits' => $this->pluck('processingUnits', $args, $required), - 'state' => $this->pluck('state', $args, $required), - 'labels' => $this->pluck('labels', $args, $required), - ], $argsCopy); - } - - /** - * @param array $instanceArray - * @return FieldMask - */ - private function fieldMask($instanceArray) - { - $mask = []; - foreach (array_keys($instanceArray) as $key) { - if ($key !== 'name') { - $mask[] = Serializer::toSnakeCase($key); - } - } - return $this->serializer->decodeMessage(new FieldMask(), ['paths' => $mask]); - } - - /** - * @param mixed $param - * @return Value - */ - private function fieldValue($param) - { - $field = new Value(); - $value = $this->formatValueForApi($param); - - $setter = null; - 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'; - $modifiedParams = []; - foreach ($param as $key => $value) { - $modifiedParams[$key] = $this->fieldValue($value); - } - $value = new Struct(); - $value->setFields($modifiedParams); - - break; - case 'list_value': - $setter = 'setListValue'; - $modifiedParams = []; - foreach ($param as $item) { - $modifiedParams[] = $this->fieldValue($item); - } - $list = new ListValue(); - $list->setValues($modifiedParams); - $value = $list; - - break; - } - - $value = is_array($value) ? current($value) : $value; - if ($setter) { - $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; - } - - if (isset($transactionOptions['readLockMode'])) { - if (isset($transactionOptions['readOnly'])) { - throw new ValidationException( - 'The readLockMode option is only valid for readWrite transactions.' - ); - } - - if (!isset($transactionOptions['readWrite'])) { - $transactionOptions['readWrite'] = []; - } - } - - if (isset($transactionOptions['readWrite'])) { - $rw = $transactionOptions['readWrite']; - - // Format nested options inside readWrite transaction - if (isset($transactionOptions['readLockMode'])) { - $rw['readLockMode'] = $transactionOptions['readLockMode']; - - // Unset the readLockMode key on the base options array. - // If we don't do this it causes issues in the serializer for TransactionOptions - unset($transactionOptions['readLockMode']); - } - - $transactionOptions['readWrite'] = $rw; - } - return $transactionOptions; - } - - /** - * Allow lazy instantiation of the instance admin client. - * - * @return InstanceAdminClient - */ - private function getInstanceAdminClient() - { - //@codeCoverageIgnoreStart - if ($this->instanceAdminClient) { - return $this->instanceAdminClient; - } - //@codeCoverageIgnoreEnd - $this->instanceAdminClient = $this->constructGapic(InstanceAdminClient::class, $this->grpcConfig); - - return $this->instanceAdminClient; - } - - /** - * Allow lazy instantiation of the database admin client. - * - * @return DatabaseAdminClient - */ - private function getDatabaseAdminClient() - { - //@codeCoverageIgnoreStart - if ($this->databaseAdminClient) { - return $this->databaseAdminClient; - } - //@codeCoverageIgnoreEnd - - $this->databaseAdminClient = $this->constructGapic(DatabaseAdminClient::class, $this->grpcConfig); - - return $this->databaseAdminClient; - } - - private function deserializeOperationArray($operation) - { - $operation['metadata'] = - $this->deserializeMessageArray($operation['metadata']) + - ['typeUrl' => $operation['metadata']['typeUrl']]; - - if (isset($operation['response']) and isset($operation['response']['typeUrl'])) { - $operation['response'] = $this->deserializeMessageArray($operation['response']); - } - - return $operation; - } - - private function deserializeMessageArray($message) - { - $typeUrl = $message['typeUrl']; - $mapper = $this->getLroResponseMapper($typeUrl); - if (!isset($mapper)) { - return $message; - } - - $className = $mapper['message']; - $response = new $className(); - $response->mergeFromString($message['value']); - return $this->serializer->encodeMessage($response); - } - - private function getLroResponseMapper($typeUrl) - { - foreach ($this->lroResponseMappers as $mapper) { - if ($mapper['typeUrl'] == $typeUrl) { - return $mapper; - } - } - - return null; - } - - /** - * Set DirectedReadOptions if provided. - * - * @param array $args - */ - private function setDirectedReadOptions(array &$args) - { - $directedReadOptions = $this->pluck('directedReadOptions', $args, false); - if (!empty($directedReadOptions)) { - $args['directedReadOptions'] = $this->serializer->decodeMessage( - new DirectedReadOptions(), - $directedReadOptions - ); - } - } - - /** - * Utiltiy method to take in a Google\Cloud\Spanner\V1\Type value and return - * the data as an array. The method takes care of array and struct elements. - * - * @param Type $type The "type" object - * - * @return array The formatted data. - */ - private function getTypeData(Type $type): array - { - $data = [ - 'code' => $type->getCode(), - 'typeAnnotation' => $type->getTypeAnnotation(), - 'protoTypeFqn' => $type->getProtoTypeFqn() - ]; - - // If this is a struct field, then recursisevly call getTypeData - if ($type->hasStructType()) { - $nestedType = $type->getStructType(); - $fields = $nestedType->getFields(); - $data['structType'] = [ - 'fields' => $this->getFieldDataFromRepeatedFields($fields) - ]; - } - // If this is an array field, then recursisevly call getTypeData - if ($type->hasArrayElementType()) { - $nestedType = $type->getArrayElementType(); - $data['arrayElementType'] = $this->getTypeData($nestedType); - } - - return $data; - } - - /** - * Utility method to return "fields data" in the format: - * [ - * "name" => "" - * "type" => [] - * ]. - * - * The type is converted from a string like INT64 to ["code" => 2, "typeAnnotation" => 0] - * conforming with the Google\Cloud\Spanner\V1\TypeCode class. - * - * @param ?RepeatedField $fields The array contain list of fields. - * - * @return array The formatted fields data. - */ - private function getFieldDataFromRepeatedFields(?RepeatedField $fields): array - { - if (is_null($fields)) { - return []; - } - - $fieldsData = []; - foreach ($fields as $key => $field) { - $type = $field->getType(); - $typeData = $this->getTypeData($type); - - $fieldsData[$key] = [ - 'name' => $field->getName(), - 'type' => $typeData - ]; - } - - return $fieldsData; - } - - private function parseMutations($rawMutations) - { - if (!is_array($rawMutations)) { - return []; - } - - $mutations = []; - foreach ($rawMutations as $mutation) { - $type = array_keys($mutation)[0]; - $data = $mutation[$type]; - - switch ($type) { - case Operation::OP_DELETE: - if (isset($data['keySet'])) { - $data['keySet'] = $this->formatKeySet($data['keySet']); - } - - $operation = $this->serializer->decodeMessage( - new Delete(), - $data - ); - break; - default: - $operation = new Write(); - $operation->setTable($data['table']); - $operation->setColumns($data['columns']); - - $modifiedData = []; - foreach ($data['values'] as $key => $param) { - $modifiedData[$key] = $this->fieldValue($param); - } - - $list = new ListValue(); - $list->setValues($modifiedData); - $values = [$list]; - $operation->setValues($values); - - break; - } - - $setterName = $this->mutationSetters[$type]; - $mutation = new Mutation(); - $mutation->$setterName($operation); - $mutations[] = $mutation; - } - return $mutations; - } -} diff --git a/Spanner/src/Connection/IamDatabase.php b/Spanner/src/Connection/IamDatabase.php deleted file mode 100644 index 74d4f27f3aba..000000000000 --- a/Spanner/src/Connection/IamDatabase.php +++ /dev/null @@ -1,65 +0,0 @@ -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/Spanner/src/Connection/IamInstance.php b/Spanner/src/Connection/IamInstance.php deleted file mode 100644 index 29fdf910b2a2..000000000000 --- a/Spanner/src/Connection/IamInstance.php +++ /dev/null @@ -1,65 +0,0 @@ -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/Spanner/src/Connection/LongRunningConnection.php b/Spanner/src/Connection/LongRunningConnection.php deleted file mode 100644 index 529bc2354583..000000000000 --- a/Spanner/src/Connection/LongRunningConnection.php +++ /dev/null @@ -1,75 +0,0 @@ -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/Spanner/src/Database.php b/Spanner/src/Database.php index 7772f54ab19b..885166a3ae6a 100644 --- a/Spanner/src/Database.php +++ b/Spanner/src/Database.php @@ -17,30 +17,55 @@ namespace Google\Cloud\Spanner; +use Closure; +use DateTimeInterface; +use Generator; use Google\ApiCore\ApiException; +use Google\ApiCore\Options\CallOptions; +use Google\ApiCore\RetrySettings; use Google\ApiCore\ValidationException; use Google\Cloud\Core\Exception\AbortedException; use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\Exception\ServiceException; -use Google\Cloud\Core\Iam\Iam; +use Google\Cloud\Core\Iam\IamManager; use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\LongRunning\LongRunningClientConnection; use Google\Cloud\Core\LongRunning\LongRunningOperation; -use Google\Cloud\Core\LongRunning\LROTrait; +use Google\Cloud\Core\OptionsValidator; +use Google\Cloud\Core\RequestHandler; use Google\Cloud\Core\Retry; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\CreateDatabaseRequest; use Google\Cloud\Spanner\Admin\Database\V1\Database\State; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; -use Google\Cloud\Spanner\Connection\ConnectionInterface; -use Google\Cloud\Spanner\Connection\IamDatabase; +use Google\Cloud\Spanner\Admin\Database\V1\DropDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseDdlRequest; +use Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupOperationsRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListDatabaseOperationsRequest; +use Google\Cloud\Spanner\Admin\Database\V1\RestoreDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\UpdateDatabaseDdlRequest; +use Google\Cloud\Spanner\Admin\Database\V1\UpdateDatabaseRequest; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; -use Google\Cloud\Spanner\V1\SpannerClient as GapicSpannerClient; +use Google\Cloud\Spanner\V1\BatchCreateSessionsRequest; +use Google\Cloud\Spanner\V1\BatchWriteRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\DeleteSessionRequest; +use Google\Cloud\Spanner\V1\Mutation; +use Google\Cloud\Spanner\V1\Mutation\Delete; +use Google\Cloud\Spanner\V1\Mutation\Write; use Google\Cloud\Spanner\V1\TransactionOptions; use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; use Google\Cloud\Spanner\V1\TypeCode; +use Google\LongRunning\ListOperationsRequest; +use Google\LongRunning\Operation as OperationProto; +use Google\Protobuf\Duration; +use Google\Protobuf\ListValue; +use Google\Protobuf\Struct; +use Google\Protobuf\Value; use Google\Rpc\Code; +use GuzzleHttp\Promise\PromiseInterface; /** * Represents a Cloud Spanner Database. @@ -49,7 +74,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $database = $spanner->connect('my-instance', 'my-database'); * ``` @@ -58,52 +83,16 @@ * // Databases can also be connected to via an Instance. * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $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 { - use LROTrait; use TransactionConfigurationTrait; - use RequestHeaderTrait; + use RequestTrait; const STATE_CREATING = State::CREATING; const STATE_READY = State::READY; @@ -128,71 +117,25 @@ class Database const TYPE_PG_OID = 'pgOid'; const TYPE_INTERVAL = TypeCode::INTERVAL; - /** - * @var ConnectionInterface - * @internal - */ - private $connection; - - /** - * @var Instance - */ - private $instance; - - /** - * @var Operation - */ - private $operation; - - /** - * @var string - */ - private $projectId; - - /** - * @var string - */ - private $name; - - /** - * @var array - */ - private $info; - - /** - * @var Iam|null - */ - private $iam; - - /** - * @var Session|null - */ - private $session; - - /** - * @var SessionPoolInterface|null - */ - private $sessionPool; - - /** - * @var bool - */ - private $isRunningTransaction = false; - - /** - * @var string|null - */ - private $databaseRole; - - /** - * @var array - */ - private $directedReadOptions; - - /** - * @var bool - */ - private $returnInt64AsObject; + private Operation $operation; + private IamManager|null $iam = null; + private Session|null $session = null; + private bool $isRunningTransaction = false; + private array $directedReadOptions; + private bool $routeToLeader; + private array $defaultQueryOptions; + private string $databaseRole; + private bool $returnInt64AsObject; + private SessionPoolInterface|null $sessionPool; + private array $info; + + private const MUTATION_SETTERS = [ + 'insert' => 'setInsert', + 'update' => 'setUpdate', + 'insertOrUpdate' => 'setInsertOrUpdate', + 'replace' => 'setReplace', + 'delete' => 'setDelete' + ]; /** * @var int @@ -202,54 +145,65 @@ class Database /** * Create an object representing a Database. * - * @param ConnectionInterface $connection The connection to the - * Cloud Spanner Admin API. This object is created by SpannerClient, - * and should not be instantiated outside of this client. + * @internal Database is constructed by the {@see Instance} class. + * + * @param SpannerClient $spannerClient The Spanner client used to interact with the API. + * @param DatabaseAdminClient $databaseAdminClient The database admin client used to interact with the API. + * @param Serializer $serializer The serializer instance to encode/decode messages. * @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 array $lroCallables * @param string $projectId The project ID. * @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 - * be returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit - * platform compatibility. **Defaults to** false. - * @param string $databaseRole The user created database role which creates the session. - * @param int $isolationLevel The level of Isolation for the transactions executed by this Client's instance. + * @param array $options [Optional] { + * Database options. + * + * @type bool $routeToLeader Enable/disable Leader Aware Routing. + * **Defaults to** `true` (enabled). + * @type array $defaultQueryOptions + * @type SessionPoolInterface $sessionPool The session pool + * implementation. + * @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. + * @type string $databaseRole The user created database role which + * creates the session. + * @type array $database The database info. + * @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance. * **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED + * } */ public function __construct( - ConnectionInterface $connection, - Instance $instance, - LongRunningConnectionInterface $lroConnection, - array $lroCallables, - $projectId, - $name, - ?SessionPoolInterface $sessionPool = null, - $returnInt64AsObject = false, - array $info = [], - $databaseRole = '', - $isolationLevel = IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED + private SpannerClient $spannerClient, + private DatabaseAdminClient $databaseAdminClient, + private Serializer $serializer, + private Instance $instance, + private string $projectId, + private string $name, + array $options = [], ) { - $this->connection = $connection; - $this->instance = $instance; - $this->projectId = $projectId; $this->name = $this->fullyQualifiedDatabaseName($name); - $this->sessionPool = $sessionPool; - $this->operation = new Operation($connection, $returnInt64AsObject); - $this->info = $info; + $this->routeToLeader = $options['routeToLeader'] ?? true; + $this->defaultQueryOptions = $options['defaultQueryOptions'] ?? []; + $this->databaseRole = $options['databaseRole'] ?? ''; + $this->returnInt64AsObject = $options['returnInt64AsObject'] ?? false; + $this->sessionPool = $options['sessionPool'] ?? null; + $this->info = $options['database'] ?? []; + $this->isolationLevel = $options['isolationLevel'] ?? IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED; + $this->operation = new Operation( + $this->spannerClient, + $serializer, + [ + 'routeToLeader' => $this->routeToLeader, + 'defaultQueryOptions' => $this->defaultQueryOptions, + 'returnInt64AsObject' => $this->returnInt64AsObject, + ] + ); if ($this->sessionPool) { $this->sessionPool->setDatabase($this); } - $this->setLroProperties($lroConnection, $lroCallables, $this->name); - $this->databaseRole = $databaseRole; + $this->optionsValidator = new OptionsValidator($serializer); $this->directedReadOptions = $instance->directedReadOptions(); - $this->returnInt64AsObject = $returnInt64AsObject; - $this->isolationLevel = $isolationLevel; } /** @@ -271,7 +225,7 @@ public function __construct( * @param array $options [optional] Configuration options. * @return int|null */ - public function state(array $options = []) + public function state(array $options = []): int|null { $info = $this->info($options); @@ -303,7 +257,7 @@ public function state(array $options = []) * * @return ItemIterator */ - public function backups(array $options = []) + public function backups(array $options = []): ItemIterator { $filter = 'database:' . $this->name(); @@ -325,7 +279,7 @@ public function backups(array $options = []) * ``` * * @param string $name The backup name. - * @param \DateTimeInterface $expireTime ​The expiration time of the backup, + * @param DateTimeInterface $expireTime ​The expiration time of the backup, * with microseconds granularity that must be at least 6 hours and * at most 366 days. Once the expireTime has passed, the backup is * eligible to be automatically deleted by Cloud Spanner. @@ -333,8 +287,11 @@ public function backups(array $options = []) * * @return LongRunningOperation */ - public function createBackup($name, \DateTimeInterface $expireTime, array $options = []) - { + public function createBackup( + string $name, + DateTimeInterface $expireTime, + array $options = [] + ): LongRunningOperation { $backup = $this->instance->backup($name); return $backup->create($this->name(), $expireTime, $options); } @@ -349,7 +306,7 @@ public function createBackup($name, \DateTimeInterface $expireTime, array $optio * * @return string */ - public function name() + public function name(): string { return $this->name; } @@ -369,7 +326,7 @@ public function name() * @param array $options [optional] Configuration options. * @return array */ - public function info(array $options = []) + public function info(array $options = []): array { return $this->info ?: $this->reload($options); } @@ -389,11 +346,23 @@ public function info(array $options = []) * @param array $options [optional] Configuration options. * @return array */ - public function reload(array $options = []) + public function reload(array $options = []): array { - return $this->info = $this->connection->getDatabase([ - 'name' => $this->name - ] + $options); + /** + * @var GetDatabaseRequest $getDatabase + * @var array $callOptions + */ + [$getDatabase, $callOptions] = $this->validateOptions( + $options, + new GetDatabaseRequest(), + CallOptions::class + ); + $getDatabase->setName($this->name); + + $response = $this->databaseAdminClient->getDatabase($getDatabase, $callOptions + [ + 'resource-prefix' => $this->name, + ]); + return $this->info = $this->handleResponse($response); } /** @@ -413,7 +382,7 @@ public function reload(array $options = []) * @param array $options [optional] Configuration options. * @return bool */ - public function exists(array $options = []) + public function exists(array $options = []): bool { try { $this->reload($options); @@ -447,20 +416,29 @@ public function exists(array $options = []) * } * @return LongRunningOperation */ - public function create(array $options = []) + public function create(array $options = []): LongRunningOperation { - $statements = $this->pluck('statements', $options, false) ?: []; - $dialect = $options['databaseDialect'] ?? null; - - $createStatement = $this->getCreateDbStatement($dialect); + $dialect = $options['databaseDialect'] ?? DatabaseDialect::DATABASE_DIALECT_UNSPECIFIED; + $options += [ + 'parent' => $this->instance->name(), + 'createStatement' => $this->getCreateDbStatement($dialect), + 'extraStatements' => $this->pluck('statements', $options, false) ?: [] + ]; - $operation = $this->connection->createDatabase([ - 'instance' => $this->instance->name(), - 'createStatement' => $createStatement, - 'extraStatements' => $statements - ] + $options); + /** + * @var CreateDatabaseRequest $createDatabase + * @var array $callOptions + */ + [$createDatabase, $callOptions] = $this->validateOptions( + $options, + new CreateDatabaseRequest(), + CallOptions::class, + ); - return $this->resumeOperation($operation['name'], $operation); + $operation = $this->databaseAdminClient->createDatabase($createDatabase, $callOptions + [ + 'resource-prefix' => $this->instance->name(), + ]); + return $this->operationFromOperationResponse($operation); } /** @@ -479,7 +457,7 @@ public function create(array $options = []) * * @return LongRunningOperation */ - public function restore($backup, array $options = []) + public function restore(Backup|string $backup, array $options = []): LongRunningOperation { return $this->instance->createDatabaseFromBackup($this->name, $backup, $options); } @@ -504,21 +482,35 @@ public function restore($backup, array $options = []) * } * @return LongRunningOperation */ - public function updateDatabase(array $options = []) + public function updateDatabase(array $options = []): LongRunningOperation { $fieldMask = []; if (isset($options['enableDropProtection'])) { $fieldMask[] = 'enable_drop_protection'; } - return $this->connection->updateDatabase([ + $options += [ + 'updateMask' => ['paths' => $fieldMask], 'database' => [ 'name' => $this->name, - 'enableDropProtection' => $options['enableDropProtection'] ?? false, - ], - 'updateMask' => [ - 'paths' => $fieldMask + 'enableDropProtection' => + $this->pluck('enableDropProtection', $options, false) ?: false ] - ] + $options); + ]; + + /** + * @var UpdateDatabaseRequest $updateDatabase + * @var array $callOptions + */ + [$updateDatabase, $callOptions] = $this->validateOptions( + $options, + new UpdateDatabaseRequest(), + CallOptions::class + ); + + $operation = $this->databaseAdminClient->updateDatabase($updateDatabase, $callOptions + [ + 'resource-prefix' => $this->name, + ]); + return $this->operationFromOperationResponse($operation); } /** @@ -544,9 +536,9 @@ public function updateDatabase(array $options = []) * * @param string $statement A DDL statements to run against a database. * @param array $options [optional] Configuration options. - * @return LongRunningOperation + * @return LongRunningOperation */ - public function updateDdl($statement, array $options = []) + public function updateDdl(string $statement, array $options = []): LongRunningOperation { return $this->updateDdlBatch([$statement], $options); } @@ -578,17 +570,32 @@ public function updateDdl($statement, array $options = []) * @codingStandardsIgnoreEnd * * @param string[] $statements A list of DDL statements to run against a database. - * @param array $options [optional] Configuration options. - * @return LongRunningOperation + * @param array $options Configuration options. Supports setting the fields + * of {@see UpdateDatabaseDdlRequest} and {@see CallOptions}. + * @return LongRunningOperation */ - public function updateDdlBatch(array $statements, array $options = []) + public function updateDdlBatch(array $statements, array $options = []): LongRunningOperation { - $operation = $this->connection->updateDatabaseDdl($options + [ - 'name' => $this->name, - 'statements' => $statements, + $options += [ + 'database' => $this->name, + 'statements' => $statements + ]; + + /** + * @var UpdateDatabaseDdlRequest $updateDatabaseDdl + * @var array $callOptions + */ + [$updateDatabaseDdl, $callOptions] = $this->validateOptions( + $options, + new UpdateDatabaseDdlRequest(), + CallOptions::class, + ); + + $operation = $this->databaseAdminClient->updateDatabaseDdl($updateDatabaseDdl, $callOptions + [ + 'resource-prefix' => $this->name ]); - return $this->resumeOperation($operation['name'], $operation); + return $this->operationFromOperationResponse($operation); } /** @@ -610,13 +617,25 @@ public function updateDdlBatch(array $statements, array $options = []) * @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. + * @param array $options Configuration options. Supports setting the fields + * of {@see DropDatabaseRequest} and {@see CallOptions}. * @return void */ - public function drop(array $options = []) + public function drop(array $options = []): void { - $this->connection->dropDatabase($options + [ - 'name' => $this->name + /** + * @var DropDatabaseRequest $dropDatabase + * @var array $callOptions + */ + [$dropDatabase, $callOptions] = $this->validateOptions( + $options, + new DropDatabaseRequest(), + CallOptions::class, + ); + $dropDatabase->setDatabase($this->name); + + $this->databaseAdminClient->dropDatabase($dropDatabase, $callOptions + [ + 'resource-prefix' => $this->name ]); if ($this->sessionPool) { @@ -643,14 +662,27 @@ public function drop(array $options = []) * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#getdatabaseddlrequest GetDatabaseDdlRequest * @codingStandardsIgnoreEnd * - * @param array $options [optional] Configuration options. + * @param array $options Configuration options. Supports setting the fields + * of {@see GetDatabaseDdlRequest} and {@see CallOptions}. * @return array */ - public function ddl(array $options = []) + public function ddl(array $options = []): array { - $ddl = $this->connection->getDatabaseDDL($options + [ - 'name' => $this->name + /** + * @var GetDatabaseDdlRequest $getDatabaseDdl + * @var array $callOptions + */ + [$getDatabaseDdl, $callOptions] = $this->validateOptions( + $options, + new GetDatabaseDdlRequest(), + CallOptions::class, + ); + $getDatabaseDdl->setDatabase($this->name); + + $response = $this->databaseAdminClient->getDatabaseDdl($getDatabaseDdl, $callOptions + [ + 'resource-prefix' => $this->name ]); + $ddl = $this->handleResponse($response); if (isset($ddl['statements'])) { return $ddl['statements']; @@ -667,13 +699,15 @@ public function ddl(array $options = []) * $iam = $database->iam(); * ``` * - * @return Iam + * @return IamManager */ - public function iam() + public function iam(): IamManager { if (!$this->iam) { - $this->iam = new Iam( - new IamDatabase($this->connection), + $this->iam = new IamManager( + new RequestHandler($this->serializer, [$this->databaseAdminClient]), + $this->serializer, + DatabaseAdminClient::class, $this->name ); } @@ -738,17 +772,13 @@ public function iam() * **Defaults to** `false`. * @type array $sessionOptions Session configuration and request options. * Session labels may be applied using the `labels` key. - * @type array $directedReadOptions Directed read options. - * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} - * If using the `replicaSelection::type` setting, utilize the constants available in - * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } - * @return Snapshot + * @return TransactionalReadInterface * @throws \BadMethodCallException If attempting to call this method within * an existing transaction. * @codingStandardsIgnoreEnd */ - public function snapshot(array $options = []) + public function snapshot(array $options = []): TransactionalReadInterface { if ($this->isRunningTransaction) { throw new \BadMethodCallException('Nested transactions are not supported by this client.'); @@ -758,10 +788,18 @@ public function snapshot(array $options = []) 'singleUse' => false ]; - $options['transactionOptions'] = $this->configureSnapshotOptions($options); - $options['directedReadOptions'] = $this->configureDirectedReadOptions( - $options, - $this->directedReadOptions ?? [] + $options['transactionOptions'] = $this->configureReadOnlyTransactionOptions($options); + + // For backwards compatibility - remove all PBReadOnly fields + // This was previously being done in configureReadOnlyTransactionOptions + // @TODO: clean this up + unset( + $options['returnReadTimestamp'], + $options['strong'], + $options['readTimestamp'], + $options['exactStaleness'], + $options['minReadTimestamp'], + $options['maxStaleness'], ); $session = $this->selectSession( @@ -810,23 +848,21 @@ public function snapshot(array $options = []) * Session labels may be applied using the `labels` key. * @type string $tag A transaction tag. Requests made using this transaction will * use this as the transaction tag. - * @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance. - * **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED * } * @return Transaction * @throws \BadMethodCallException If attempting to call this method within * an existing transaction. */ - public function transaction(array $options = []) + public function transaction(array $options = []): Transaction { if ($this->isRunningTransaction) { throw new \BadMethodCallException('Nested transactions are not supported by this client.'); } // Configure readWrite options here. Any nested options for readWrite should be added to this call - $options['transactionOptions'] = $this->configureTransactionOptions($options['transactionOptions'] ?? [ - 'isolationLevel' => $options['isolationLevel'] ?? $this->isolationLevel - ]); + $options['transactionOptions'] = $this->configureReadWriteTransactionOptions( + ($options['transactionOptions'] ?? []) + ['isolationLevel' => $this->isolationLevel] + ); $session = $this->selectSession( SessionPoolInterface::CONTEXT_READWRITE, @@ -903,8 +939,12 @@ public function transaction(array $options = []) * @param array $options [optional] { * Configuration Options * - * @type int $maxRetries The number of times to attempt to apply the - * operation before failing. **Defaults to ** `10`. + * @type RetrySettings|array $retrySettings { + * Retry configuration options. Currently, only the `maxRetries` option is supported. + * + * @type int $maxRetries The maximum number of retry attempts before the operation fails. + * 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 @@ -924,21 +964,25 @@ public function transaction(array $options = []) * @throws \BadMethodCallException If attempting to call this method within * an existing transaction. */ - public function runTransaction(callable $operation, array $options = []) + public function runTransaction(callable $operation, array $options = []): mixed { if ($this->isRunningTransaction) { throw new \BadMethodCallException('Nested transactions are not supported by this client.'); } + $options += ['retrySettings' => ['maxRetries' => self::MAX_RETRIES]]; - $options += [ - 'maxRetries' => self::MAX_RETRIES, - ]; - - $transactionOptions = (isset($options['transactionOptions'])) ? $options['transactionOptions'] : []; + $retrySettings = $this->pluck('retrySettings', $options); + if ($retrySettings instanceof RetrySettings) { + $maxRetries = $retrySettings->getMaxRetries(); + } else { + $maxRetries = $retrySettings['maxRetries']; + } - $options['transactionOptions'] = $this->configureTransactionOptions([ - 'isolationLevel' => $options['transactionOptions']['isolationLevel'] ?? $this->isolationLevel - ] + $transactionOptions); + // Configure necessary readWrite nested and base options + $transactionOptions = $options['transactionOptions'] ?? []; + $options['transactionOptions'] = $this->configureReadWriteTransactionOptions( + $transactionOptions + ['isolationLevel' => $this->isolationLevel] + ); $session = $this->selectSession( SessionPoolInterface::CONTEXT_READWRITE, @@ -947,7 +991,6 @@ public function runTransaction(callable $operation, array $options = []) $attempt = 0; $startTransactionFn = function ($session, $options) use (&$attempt) { - // Initial attempt requires to set `begin` options (ILB). if ($attempt === 0) { if (!isset($options['transactionOptions']['isolationLevel'])) { @@ -1005,7 +1048,7 @@ public function runTransaction(callable $operation, array $options = []) return $res; }; - $retry = new Retry($options['maxRetries'], $delayFn); + $retry = new Retry($maxRetries, $delayFn); try { return $retry->execute($transactionFn, [$operation, $session, $options]); @@ -1052,7 +1095,7 @@ public function runTransaction(callable $operation, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function insert($table, array $data, array $options = []) + public function insert(string $table, array $data, array $options = []): Timestamp { return $this->insertBatch($table, [$data], $options); } @@ -1101,7 +1144,7 @@ public function insert($table, array $data, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function insertBatch($table, array $dataSet, array $options = []) + public function insertBatch(string $table, array $dataSet, array $options = []): Timestamp { $mutations = []; foreach ($dataSet as $data) { @@ -1147,7 +1190,7 @@ public function insertBatch($table, array $dataSet, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function update($table, array $data, array $options = []) + public function update(string $table, array $data, array $options = []): Timestamp { return $this->updateBatch($table, [$data], $options); } @@ -1193,7 +1236,7 @@ public function update($table, array $data, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function updateBatch($table, array $dataSet, array $options = []) + public function updateBatch(string $table, array $dataSet, array $options = []): Timestamp { $mutations = []; foreach ($dataSet as $data) { @@ -1240,7 +1283,7 @@ public function updateBatch($table, array $dataSet, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function insertOrUpdate($table, array $data, array $options = []) + public function insertOrUpdate(string $table, array $data, array $options = []): Timestamp { return $this->insertOrUpdateBatch($table, [$data], $options); } @@ -1288,7 +1331,7 @@ public function insertOrUpdate($table, array $data, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function insertOrUpdateBatch($table, array $dataSet, array $options = []) + public function insertOrUpdateBatch(string $table, array $dataSet, array $options = []): Timestamp { $mutations = []; foreach ($dataSet as $data) { @@ -1335,7 +1378,7 @@ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function replace($table, array $data, array $options = []) + public function replace(string $table, array $data, array $options = []): Timestamp { return $this->replaceBatch($table, [$data], $options); } @@ -1383,7 +1426,7 @@ public function replace($table, array $data, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function replaceBatch($table, array $dataSet, array $options = []) + public function replaceBatch(string $table, array $dataSet, array $options = []): Timestamp { $mutations = []; foreach ($dataSet as $data) { @@ -1433,7 +1476,7 @@ public function replaceBatch($table, array $dataSet, array $options = []) * } * @return Timestamp The commit Timestamp. */ - public function delete($table, KeySet $keySet, array $options = []) + public function delete(string $table, KeySet $keySet, array $options = []): Timestamp { $mutations = [$this->operation->deleteMutation($table, $keySet)]; @@ -1693,7 +1736,7 @@ public function delete($table, KeySet $keySet, array $options = []) * @codingStandardsIgnoreEnd * @return Result */ - public function execute($sql, array $options = []) + public function execute(string $sql, array $options = []): Result { unset($options['requestOptions']['transactionTag']); $session = $this->pluck('session', $options, false) @@ -1706,7 +1749,6 @@ public function execute($sql, array $options = []) $options['transaction'], $options['transactionContext'] ) = $this->transactionSelector($options); - $options = $this->addLarHeader($options, true, $options['transactionContext']); if (isset($options['transaction']['readWrite'])) { $options['transaction']['begin']['isolationLevel'] ??= $this->isolationLevel; @@ -1714,11 +1756,15 @@ public function execute($sql, array $options = []) $options['directedReadOptions'] = $this->configureDirectedReadOptions( $options, - $this->directedReadOptions ?? [] + $this->directedReadOptions ); try { - return $this->operation->execute($session, $sql, $options); + // Unset the internal flag. + unset($options['singleUse']); + return $this->operation->execute($session, $sql, $options + [ + 'route-to-leader' => $options['transactionContext'] === SessionPoolInterface::CONTEXT_READWRITE + ]); } finally { $session->setExpiration(); } @@ -1729,7 +1775,7 @@ public function execute($sql, array $options = []) * * @return MutationGroup */ - public function mutationGroup() + public function mutationGroup(): MutationGroup { return new MutationGroup($this->returnInt64AsObject); } @@ -1776,11 +1822,11 @@ public function mutationGroup() * transactions. * } * - * @return \Generator {@see \Google\Cloud\Spanner\V1\BatchWriteResponse} + * @return Generator {@see \Google\Cloud\Spanner\V1\BatchWriteResponse} * * @throws ApiException if the remote call fails */ - public function batchWrite(array $mutationGroups, array $options = []) + public function batchWrite(array $mutationGroups, array $options = []): Generator { if ($this->isRunningTransaction) { throw new \BadMethodCallException('Nested transactions are not supported by this client.'); @@ -1794,12 +1840,32 @@ public function batchWrite(array $mutationGroups, array $options = []) $mutationGroups = array_map(fn ($x) => $x->toArray(), $mutationGroups); + array_walk( + $mutationGroups, + fn (&$x) => $x['mutations'] = $this->parseMutations($x['mutations']) + ); + try { - return $this->connection->batchWrite([ - 'database' => $this->name(), + $options += [ 'session' => $session->name(), 'mutationGroups' => $mutationGroups - ] + $options); + ]; + + /** + * @var BatchWriteRequest $batchWrite + * @var array $callOptions + */ + [$batchWrite, $callOptions] = $this->validateOptions( + $options, + new BatchWriteRequest(), + CallOptions::class + ); + + $response = $this->spannerClient->batchWrite($batchWrite, $callOptions + [ + 'resource-prefix' => $this->name, + 'route-to-leader' => $this->routeToLeader, + ]); + return $this->handleResponse($response); } finally { $this->isRunningTransaction = false; $session->setExpiration(); @@ -1926,7 +1992,7 @@ public function batchWrite(array $mutationGroups, array $options = []) * @return int The number of rows modified. * @throws ValidationException */ - public function executePartitionedUpdate($statement, array $options = []) + public function executePartitionedUpdate($statement, array $options = []): int { unset($options['requestOptions']['transactionTag']); @@ -1945,15 +2011,15 @@ public function executePartitionedUpdate($statement, array $options = []) if (isset($options['transactionOptions']['excludeTxnFromChangeStreams'])) { $beginTransactionOptions['transactionOptions']['excludeTxnFromChangeStreams'] = $options['transactionOptions']['excludeTxnFromChangeStreams']; + unset($options['transactionOptions']); } $transaction = $this->operation->transaction($session, $beginTransactionOptions); - $options = $this->addLarHeader($options); - try { return $this->operation->executeUpdate($session, $transaction, $statement, [ - 'statsItem' => 'rowCountLowerBound' + 'statsItem' => 'rowCountLowerBound', + 'route-to-leader' => true, ] + $options); } finally { $session->setExpiration(); @@ -2083,7 +2149,7 @@ public function executePartitionedUpdate($statement, array $options = []) * @codingStandardsIgnoreEnd * @return Result */ - public function read($table, KeySet $keySet, array $columns, array $options = []) + public function read($table, KeySet $keySet, array $columns, array $options = []): Result { unset($options['requestOptions']['transactionTag']); $session = $this->selectSession( @@ -2097,13 +2163,15 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] $options['directedReadOptions'] = $this->configureDirectedReadOptions( $options, - $this->directedReadOptions ?? [] + $this->directedReadOptions ); - $options = $this->addLarHeader($options, true, $context); - try { - return $this->operation->read($session, $table, $keySet, $columns, $options); + // Unset the internal flag. + unset($options['singleUse']); + return $this->operation->read($session, $table, $keySet, $columns, $options + [ + 'route-to-leader' => $context === SessionPoolInterface::CONTEXT_READ + ]); } finally { $session->setExpiration(); } @@ -2119,7 +2187,7 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] * * @return SessionPoolInterface|null */ - public function sessionPool() + public function sessionPool(): ?SessionPoolInterface { return $this->sessionPool; } @@ -2137,7 +2205,7 @@ public function sessionPool() * $database->close(); * ``` */ - public function close() + public function close(): void { if ($this->session) { if ($this->sessionPool) { @@ -2175,7 +2243,7 @@ public function __destruct() * @param array $options [optional] Configuration options. * @return Session */ - public function createSession(array $options = []) + public function createSession(array $options = []): Session { return $this->operation->createSession($this->name, $options); } @@ -2192,7 +2260,7 @@ public function createSession(array $options = []) * @param string $sessionName The session's name. * @return Session */ - public function session($sessionName) + public function session(string $sessionName): Session { return $this->operation->session($sessionName); } @@ -2203,7 +2271,7 @@ public function session($sessionName) * @access private * @return array */ - public function identity() + public function identity(): array { $databaseParts = explode('/', $this->name); $instanceParts = explode('/', $this->instance->name()); @@ -2216,33 +2284,236 @@ public function identity() } /** - * Returns the underlying connection. + * Creates a batch of sessions. + * + * @param array $options { + * @type array $sessionTemplate + * @type int $sessionCount + * } + */ + public function batchCreateSessions(array $options): array + { + [$data, $callOptions] = $this->splitOptionalArgs($options); + $data['database'] = $this->name; + + $request = $this->serializer->decodeMessage(new BatchCreateSessionsRequest(), $data); + $response = $this->spannerClient->batchCreateSessions($request, $callOptions + [ + 'resource-prefix' => $this->name, + 'route-to-leader' => $this->routeToLeader + ]); + return $this->handleResponse($response); + } + + /** + * Delete session asynchronously. * * @access private - * @return ConnectionInterface + * @param array $options { + * @type name The session name to be deleted + * } + * @return PromiseInterface * @experimental */ - public function connection() + public function deleteSessionAsync(array $options): PromiseInterface { - return $this->connection; + [$data, $callOptions] = $this->splitOptionalArgs($options); + + $request = $this->serializer->decodeMessage(new DeleteSessionRequest(), $data); + return $this->spannerClient->deleteSessionAsync($request, $callOptions + [ + 'resource-prefix' => $this->name + ]); } /** - * Represent the class in a more readable and digestable fashion. + * Lists backup operations. * - * @access private - * @codeCoverageIgnore + * @param array $options [optional] { + * Configuration options. + * + * @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 int $resultLimit Limit the number of results returned in total. + * **Defaults to** `0` (return all results). + * @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. + * } + * + * @return ItemIterator */ - public function __debugInfo() + public function backupOperations(array $options = []): ItemIterator { - return [ - 'connection' => get_class($this->connection), - 'projectId' => $this->projectId, - 'name' => $this->name, - 'instance' => $this->instance, - 'sessionPool' => $this->sessionPool, - 'session' => $this->session, + /** + * @var ListBackupOperationsRequest $listBackupOperations + * @var array $callOptions + */ + [$listBackupOperations, $callOptions] = $this->validateOptions( + $options, + new ListBackupOperationsRequest(), + CallOptions::class + ); + $listBackupOperations->setParent($this->instance->name()); + + return $this->buildLongRunningIterator( + [$this->databaseAdminClient, 'listBackupOperations'], + $listBackupOperations, + $callOptions + ['resource-prefix' => $this->name], + $this->getResultMapper() + ); + } + + /** + * Create a database from a backup. + * + * @param string $name The database name. + * @param Backup|string $backup The backup to restore, given + * as a Backup instance or a string of the form + * `projects//instances//backups/`. + * @param array $options [optional] Configuration options. + * + * @return LongRunningOperation + */ + public function createDatabaseFromBackup($name, $backup, array $options = []): LongRunningOperation + { + $options += [ + 'parent' => $this->instance->name(), + 'databaseId' => $this->databaseIdOnly($name), + 'backup' => $backup instanceof Backup ? $backup->name() : $backup ]; + /** + * @var RestoreDatabaseRequest $restoreDatabase + * @var array $callOptions + */ + [$restoreDatabase, $callOptions] = $this->validateOptions( + $options, + new RestoreDatabaseRequest(), + CallOptions::class + ); + + $operation = $this->databaseAdminClient->restoreDatabase($restoreDatabase, $callOptions + [ + 'resource-prefix' => $this->name + ]); + + return $this->operationFromOperationResponse($operation); + } + + /** + * Lists database operations. + * + * @param array $options [optional] { + * Configuration options. + * + * @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 int $resultLimit Limit the number of results returned in total. + * **Defaults to** `0` (return all results). + * @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. + * } + * + * @return ItemIterator + */ + public function databaseOperations(array $options = []): ItemIterator + { + /** + * @var ListDatabaseOperationsRequest $listDatabaseOperations + * @var array $callOptions + */ + [$listDatabaseOperations, $callOptions] = $this->validateOptions( + $options, + new ListDatabaseOperationsRequest(), + CallOptions::class + ); + $listDatabaseOperations->setParent($this->instance->name()); + + return $this->buildLongRunningIterator( + [$this->databaseAdminClient, 'listDatabaseOperations'], + $listDatabaseOperations, + $callOptions + ['resource-prefix' => $this->name], + $this->getResultMapper() + ); + } + + /** + * Resume a Long Running Operation + * + * Example: + * ``` + * $operation = $database->resumeOperation($operationName); + * ``` + * + * @param string $operationName The Long Running Operation name. + * @return LongRunningOperation + */ + public function resumeOperation(string $operationName, array $options = []): LongRunningOperation + { + return new LongRunningOperation( + new LongRunningClientConnection($this->databaseAdminClient, $this->serializer), + $operationName, + [ + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata', + 'callable' => $this->databaseResultFunction(), + ], + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata', + 'callable' => $this->databaseResultFunction(), + ] + ], + $options + ); + } + + /** + * 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 + */ + public function longRunningOperations(array $options = []): ItemIterator + { + /** + * @var ListOperationsRequest $listOperationsRequest + * @var array $callOptions + */ + [$listOperationsRequest, $callOptions] = $this->validateOptions( + $options, + new ListOperationsRequest(), + CallOptions::class + ); + $listOperationsRequest->setName($this->name . '/operations'); + + return $this->buildLongRunningIterator( + [$this->databaseAdminClient->getOperationsClient(), 'listOperations'], + $listOperationsRequest, + $callOptions, + $this->getResultMapper() + ); } /** @@ -2255,8 +2526,10 @@ public function __debugInfo() * @param array $options [optional] Configuration options. * @return Session */ - private function selectSession($context = SessionPoolInterface::CONTEXT_READ, array $options = []) - { + private function selectSession( + $context = SessionPoolInterface::CONTEXT_READ, + array $options = [] + ): Session { if ($this->session) { return $this->session; } @@ -2288,7 +2561,7 @@ private function selectSession($context = SessionPoolInterface::CONTEXT_READ, ar * } * @return Timestamp The commit timestamp. */ - private function commitInSingleUseTransaction(array $mutations, array $options = []) + private function commitInSingleUseTransaction(array $mutations, array $options = []): Timestamp { unset($options['requestOptions']['transactionTag']); $options['mutations'] = $mutations; @@ -2305,12 +2578,12 @@ private function commitInSingleUseTransaction(array $mutations, array $options = * * @return string */ - private function fullyQualifiedDatabaseName($name) + private function fullyQualifiedDatabaseName($name): string { - $instance = InstanceAdminClient::parseName($this->instance->name())['instance']; + $instance = DatabaseAdminClient::parseName($this->instance->name())['instance']; try { - return GapicSpannerClient::databaseName( + return SpannerClient::databaseName( $this->projectId, $instance, $name @@ -2325,10 +2598,10 @@ private function fullyQualifiedDatabaseName($name) /** * Returns the 'CREATE DATABASE' statement as per the given database dialect * - * @param string $dialect The dialect of the database to be created + * @param int $dialect The dialect of the database to be created * @return string The specific 'CREATE DATABASE' statement */ - private function getCreateDbStatement($dialect) + private function getCreateDbStatement(int $dialect): string { $databaseId = DatabaseAdminClient::parseName($this->name())['database']; @@ -2338,4 +2611,158 @@ private function getCreateDbStatement($dialect) return sprintf('CREATE DATABASE `%s`', $databaseId); } + + /** + * Extracts a database id from fully qualified name. + * + * @param string $name The database name or id. + * @return string + */ + private function databaseIdOnly(string $name): string + { + try { + return DatabaseAdminClient::parseName($name)['database']; + } catch (ValidationException $e) { + return $name; + } + } + + private function parseMutations(array $rawMutations): array + { + if (!is_array($rawMutations)) { + return []; + } + + $mutations = []; + foreach ($rawMutations as $mutation) { + $type = array_keys($mutation)[0]; + $data = $mutation[$type]; + + switch ($type) { + case Operation::OP_DELETE: + $operation = $this->serializer->decodeMessage( + new Delete(), + $data + ); + break; + default: + $operation = new Write(); + $operation->setTable($data['table']); + $operation->setColumns($data['columns']); + + $modifiedData = []; + foreach ($data['values'] as $key => $param) { + $modifiedData[$key] = $this->fieldValue($param); + } + + $list = new ListValue(); + $list->setValues($modifiedData); + $values = [$list]; + $operation->setValues($values); + + break; + } + + $setterName = self::MUTATION_SETTERS[$type]; + $mutation = new Mutation(); + $mutation->$setterName($operation); + $mutations[] = $mutation; + } + return $mutations; + } + + /** + * @param mixed $param + * @return Value + */ + private function fieldValue($param): Value + { + $field = new Value(); + $value = $this->formatValueForApi($param); + + $setter = null; + 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'; + $modifiedParams = []; + foreach ($param as $key => $value) { + $modifiedParams[$key] = $this->fieldValue($value); + } + $value = new Struct(); + $value->setFields($modifiedParams); + + break; + case 'list_value': + $setter = 'setListValue'; + $modifiedParams = []; + foreach ($param as $item) { + $modifiedParams[] = $this->fieldValue($item); + } + $list = new ListValue(); + $list->setValues($modifiedParams); + $value = $list; + + break; + } + + $value = is_array($value) ? current($value) : $value; + if ($setter) { + $field->$setter($value); + } + + return $field; + } + + private function databaseResultFunction(): Closure + { + return function (array $database): self { + $name = DatabaseAdminClient::parseName($database['name']); + return $this->instance->database($name['database'], [ + 'sessionPool' => $this->sessionPool, + 'database' => $database, + 'databaseRole' => $this->databaseRole, + ]); + }; + } + + private function getResultMapper() + { + return function (OperationProto $operation) { + return $this->resumeOperation( + $operation->getName(), + $this->handleResponse($operation) + ); + }; + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'spannerClient' => get_class($this->spannerClient), + 'databaseAdminClient' => get_class($this->databaseAdminClient), + 'projectId' => $this->projectId, + 'name' => $this->name, + 'instance' => $this->instance, + 'sessionPool' => $this->sessionPool, + 'session' => $this->session, + ]; + } } diff --git a/Spanner/src/Date.php b/Spanner/src/Date.php index 10a831995f1e..9d97d1d08683 100644 --- a/Spanner/src/Date.php +++ b/Spanner/src/Date.php @@ -17,6 +17,8 @@ namespace Google\Cloud\Spanner; +use DateTimeInterface; + /** * Represents a value with a data type of * [Date](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TypeCode). @@ -25,7 +27,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $date = $spanner->date(new \DateTimeImmutable('1995-02-04')); * ``` @@ -40,14 +42,14 @@ class Date implements ValueInterface const FORMAT = 'Y-m-d'; /** - * @var \DateTimeInterface + * @var DateTimeInterface */ - protected $value; + protected DateTimeInterface $value; /** - * @param \DateTimeInterface $value The date value. + * @param DateTimeInterface $value The date value. */ - public function __construct(\DateTimeInterface $value) + public function __construct(DateTimeInterface $value) { $this->value = $value; } @@ -65,8 +67,11 @@ public function __construct(\DateTimeInterface $value) * @param int|string $day The day of the month. * @return Date */ - public static function createFromValues($year, $month, $day) - { + public static function createFromValues( + int|string $year, + int|string $month, + int|string $day + ): Date { $value = sprintf('%s-%s-%s', $year, $month, $day); $dt = \DateTimeImmutable::createFromFormat(self::FORMAT, $value); @@ -74,16 +79,16 @@ public static function createFromValues($year, $month, $day) } /** - * Get the underlying `\DateTimeInterface` implementation. + * Get the underlying `DateTimeInterface` implementation. * * Example: * ``` * $dateTime = $date->get(); * ``` * - * @return \DateTimeInterface + * @return DateTimeInterface */ - public function get() + public function get(): DateTimeInterface { return $this->value; } @@ -98,7 +103,7 @@ public function get() * * @return int */ - public function type() + public function type(): int { return Database::TYPE_DATE; } @@ -113,7 +118,7 @@ public function type() * * @return string */ - public function formatAsString() + public function formatAsString(): string { return $this->value->format(self::FORMAT); } @@ -124,7 +129,7 @@ public function formatAsString() * @return string * @access private */ - public function __toString() + public function __toString(): string { return $this->formatAsString(); } diff --git a/Spanner/src/Duration.php b/Spanner/src/Duration.php deleted file mode 100644 index 4fe506ac0880..000000000000 --- a/Spanner/src/Duration.php +++ /dev/null @@ -1,121 +0,0 @@ -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/Spanner/src/Instance.php b/Spanner/src/Instance.php index 6ef646012a19..b8ff3d9ec629 100644 --- a/Spanner/src/Instance.php +++ b/Spanner/src/Instance.php @@ -17,22 +17,29 @@ namespace Google\Cloud\Spanner; -use Google\ApiCore\ValidationException; -use Google\Cloud\Core\ArrayTrait; +use Closure; +use Google\ApiCore\Options\CallOptions; use Google\Cloud\Core\Exception\NotFoundException; -use Google\Cloud\Core\Iam\Iam; +use Google\Cloud\Core\Iam\IamManager; use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\Iterator\PageIterator; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\LongRunning\LongRunningClientConnection; use Google\Cloud\Core\LongRunning\LongRunningOperation; -use Google\Cloud\Core\LongRunning\LROTrait; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Core\OptionsValidator; +use Google\Cloud\Core\RequestHandler; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupsRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListDatabasesRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\CreateInstanceRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\DeleteInstanceRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\GetInstanceRequest; use Google\Cloud\Spanner\Admin\Instance\V1\Instance\State; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; -use Google\Cloud\Spanner\Connection\ConnectionInterface; -use Google\Cloud\Spanner\Connection\IamInstance; +use Google\Cloud\Spanner\Admin\Instance\V1\UpdateInstanceRequest; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient; use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; +use Google\LongRunning\ListOperationsRequest; +use Google\LongRunning\Operation as OperationProto; /** * Represents a Cloud Spanner instance @@ -41,91 +48,27 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => $projectId]); * * $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 { - use ArrayTrait; - use LROTrait; + use RequestTrait; const STATE_READY = State::READY; const STATE_CREATING = State::CREATING; const DEFAULT_NODE_COUNT = 1; - /** - * @var ConnectionInterface - * @internal - */ - private $connection; - - /** - * @var string - */ - private $projectId; - - /** - * @var string - */ - private $name; - - /** - * @var bool - */ - private $returnInt64AsObject; - - /** - * @var array - */ - private $info; - - /** - * @var Iam|null - */ - private $iam; - - /** - * @var array - */ - private $directedReadOptions; + private IamManager|null $iam = null; + private array $directedReadOptions; + private array $defaultQueryOptions; + private bool $routeToLeader; + private string $projectName; + private bool $returnInt64AsObject; + private array $info; /** * @var int @@ -135,19 +78,15 @@ class Instance /** * Create an object representing a Cloud Spanner instance. * - * @param ConnectionInterface $connection The connection to the - * Cloud Spanner Admin API. This object is created by SpannerClient, - * and should not be instantiated outside of this client. - * @param LongRunningConnectionInterface $lroConnection An implementation - * mapping to methods which handle LRO resolution in the service. - * @param array $lroCallables + * @internal Instance is constructed by the {@see SpannerClient} class. + * + * @param GapicSpannerClient $spannerClient The spanner client. + * @param InstanceAdminClient $instanceAdminClient The instance admin client. + * @param DatabaseAdminClient $databaseAdminClient The database admin client. + * @param Serializer $serializer The serializer instance to encode/decode messages. * @param string $projectId The project ID. * @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. - * @param array $info [optional] A representation of the instance object. - * @param array $options [optional]{ + * @param array $options { * Instance options * * @type array $directedReadOptions Directed read options. @@ -156,27 +95,32 @@ class Instance * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance. * **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED + * @type bool $routeToLeader Enable/disable Leader Aware Routing. + * **Defaults to** `true` (enabled). + * @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. + * @type array $instance An array representation of the instance object. * } */ public function __construct( - ConnectionInterface $connection, - LongRunningConnectionInterface $lroConnection, - array $lroCallables, - $projectId, - $name, - $returnInt64AsObject = false, - array $info = [], - array $options = [] + private GapicSpannerClient $spannerClient, + private InstanceAdminClient $instanceAdminClient, + private DatabaseAdminClient $databaseAdminClient, + private Serializer $serializer, + private string $projectId, + private string $name, + array $options = [], ) { - $this->connection = $connection; - $this->projectId = $projectId; $this->name = $this->fullyQualifiedInstanceName($name, $projectId); - $this->returnInt64AsObject = $returnInt64AsObject; - $this->info = $info; - - $this->setLroProperties($lroConnection, $lroCallables, $this->name); $this->directedReadOptions = $options['directedReadOptions'] ?? []; $this->isolationLevel = $options['isolationLevel'] ?? IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED; + $this->routeToLeader = $options['routeToLeader'] ?? true; + $this->defaultQueryOptions = $options['defaultQueryOptions'] ?? []; + $this->returnInt64AsObject = $options['returnInt64AsObject'] ?? false; + $this->info = $options['instance'] ?? []; + $this->projectName = InstanceAdminClient::projectName($projectId); + $this->optionsValidator = new OptionsValidator($serializer); } /** @@ -189,7 +133,7 @@ public function __construct( * * @return string */ - public function name() + public function name(): string { return $this->name; } @@ -216,7 +160,7 @@ public function name() * * @return array */ - public function info(array $options = []) + public function info(array $options = []): array { if (!$this->info) { $this->reload($options); @@ -240,17 +184,28 @@ public function info(array $options = []) * @param array $options [optional] Configuration options. * @return bool */ - public function exists(array $options = []) + public function exists(array $options = []): bool { try { if ($this->info) { - $this->connection->getInstance([ + $options += [ 'name' => $this->name, - 'projectName' => InstanceAdminClient::projectName( - $this->projectId - ), - 'fieldMask' => ['name'], - ] + $options); + 'fieldMask' => ['paths' => ['name']], + ]; + + /** + * @var GetInstanceRequest $getInstanceRequest + * @var array $callOptions + */ + [$getInstanceRequest, $callOptions] = $this->validateOptions( + $options, + new GetInstanceRequest(), + CallOptions::class + ); + + $this->instanceAdminClient->getInstance($getInstanceRequest, $callOptions + [ + 'resource-prefix' => $this->projectName + ]); } else { $this->reload($options); } @@ -282,14 +237,35 @@ public function exists(array $options = []) * } * @return array */ - public function reload(array $options = []) + public function reload(array $options = []): array { - $this->info = $this->connection->getInstance($options + [ - 'name' => $this->name, - 'projectName' => InstanceAdminClient::projectName($this->projectId), - ]); + /** + * @var array $data + * @var array $calloptions + */ + [$data, $callOptions] = $this->splitOptionalArgs($options); + $data += [ + 'name' => $this->name + ]; - return $this->info; + if (isset($data['fieldMask'])) { + $fieldMask = []; + if (is_array($data['fieldMask'])) { + foreach (array_values($data['fieldMask']) as $field) { + $fieldMask[] = $this->serializer::toSnakeCase($field); + } + } else { + $fieldMask[] = $this->serializer::toSnakeCase($data['fieldMask']); + } + $data['fieldMask'] = ['paths' => $fieldMask]; + } + + $request = $this->serializer->decodeMessage(new GetInstanceRequest(), $data); + + $response = $this->instanceAdminClient->getInstance($request, $callOptions + [ + 'resource-prefix' => $this->projectName + ]); + return $this->info = $this->handleResponse($response); } /** @@ -313,36 +289,39 @@ public function reload(array $options = []) * @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 + * @return LongRunningOperation * @throws \InvalidArgumentException * @codingStandardsIgnoreEnd */ - public function create(InstanceConfiguration $config, array $options = []) + public function create(InstanceConfiguration $config, array $options = []): LongRunningOperation { + /** + * @var array $instance + * @var array $calloptions + */ + [$instance, $callOptions] = $this->splitOptionalArgs($options); $instanceId = InstanceAdminClient::parseName($this->name)['instance']; - $options += [ - 'displayName' => $instanceId, - 'labels' => [], - ]; - - if (isset($options['nodeCount']) && isset($options['processingUnits'])) { + if (isset($instance['nodeCount']) && isset($instance['processingUnits'])) { throw new \InvalidArgumentException('Must only set either `nodeCount` or `processingUnits`'); } - if (empty($options['nodeCount']) && empty($options['processingUnits'])) { - $options['nodeCount'] = self::DEFAULT_NODE_COUNT; + if (empty($instance['nodeCount']) && empty($instance['processingUnits'])) { + $instance['nodeCount'] = self::DEFAULT_NODE_COUNT; } - // This must always be set to CREATING, so overwrite anything else. - $options['state'] = State::CREATING; - - $operation = $this->connection->createInstance([ + $data = [ + 'parent' => InstanceAdminClient::projectName( + $this->projectId + ), 'instanceId' => $instanceId, - 'name' => $this->name, - 'projectName' => InstanceAdminClient::projectName($this->projectId), - 'config' => $config->name() - ] + $options); + 'instance' => $this->createInstanceArray($instance, $config) + ]; + + $request = $this->serializer->decodeMessage(new CreateInstanceRequest(), $data); - return $this->resumeOperation($operation['name'], $operation); + $operation = $this->instanceAdminClient->createInstance($request, $callOptions + [ + 'resource-prefix' => $this->name + ]); + return $this->operationFromOperationResponse($operation); } /** @@ -362,15 +341,13 @@ public function create(InstanceConfiguration $config, array $options = []) * ``` * * @param array $options [optional] Configuration options. - * @return int|null + * @return int */ - public function state(array $options = []) + public function state(array $options = []): int { $info = $this->info($options); - return (isset($info['state'])) - ? $info['state'] - : null; + return $info['state'] ?? State::STATE_UNSPECIFIED; } /** @@ -402,17 +379,30 @@ public function state(array $options = []) * @return LongRunningOperation * @throws \InvalidArgumentException */ - public function update(array $options = []) + public function update(array $options = []): LongRunningOperation { + /** + * @var array $instance + * @var array $calloptions + */ + [$instance, $callOptions] = $this->splitOptionalArgs($options); + if (isset($options['nodeCount']) && isset($options['processingUnits'])) { throw new \InvalidArgumentException('Must only set either `nodeCount` or `processingUnits`'); } - $operation = $this->connection->updateInstance([ - 'name' => $this->name, - ] + $options); + $fieldMask = $this->fieldMask($instance); + $data = [ + 'fieldMask' => $fieldMask, + 'instance' => $this->createInstanceArray($instance) + ]; + + $request = $this->serializer->decodeMessage(new UpdateInstanceRequest(), $data); - return $this->resumeOperation($operation['name'], $operation); + $operation = $this->instanceAdminClient->updateInstance($request, $callOptions + [ + 'resource-prefix' => $this->name + ]); + return $this->operationFromOperationResponse($operation); } /** @@ -430,10 +420,21 @@ public function update(array $options = []) * @param array $options [optional] Configuration options. * @return void */ - public function delete(array $options = []) + public function delete(array $options = []): void { - $this->connection->deleteInstance($options + [ - 'name' => $this->name + /** + * @var DeleteInstanceRequest $deleteInstancesRequest + * @var array $callOptions + */ + [$deleteInstancesRequest, $callOptions] = $this->validateOptions( + $options, + new DeleteInstanceRequest(), + CallOptions::class + ); + $deleteInstancesRequest->setName($this->name); + + $this->instanceAdminClient->deleteInstance($deleteInstancesRequest, $callOptions + [ + 'resource-prefix' => $this->name ]); } @@ -461,9 +462,9 @@ public function delete(array $options = []) * @type SessionPoolInterface $sessionPool A pool used to manage * sessions. * } - * @return LongRunningOperation + * @return LongRunningOperation */ - public function createDatabase($name, array $options = []) + public function createDatabase($name, array $options = []): LongRunningOperation { $instantiation = $this->pluckArray(['sessionPool'], $options); @@ -485,22 +486,14 @@ public function createDatabase($name, array $options = []) * `projects//instances//backups/`. * @param array $options [optional] Configuration options. * - * @return LongRunningOperation + * @return LongRunningOperation */ - - public function createDatabaseFromBackup($name, $backup, array $options = []) - { - $backup = $backup instanceof Backup - ? $backup->name() - : $backup; - - $operation = $this->connection->restoreDatabase([ - 'instance' => $this->name(), - 'databaseId' => $this->databaseIdOnly($name), - 'backup' => $backup, - ] + $options); - - return $this->resumeOperation($operation['name'], $operation); + public function createDatabaseFromBackup( + string $name, + Backup|string $backup, + array $options = [] + ): LongRunningOperation { + return $this->database($name)->createDatabaseFromBackup($name, $backup, $options); } /** @@ -518,31 +511,40 @@ public function createDatabaseFromBackup($name, $backup, array $options = []) * ``` * * @param string $name The database name - * @param array $options [optional] { + * @param array $options { * Configuration options. * - * @type SessionPoolInterface $sessionPool A pool used to manage - * sessions. - * @type string $databaseRole The user created database role which creates the session. + * @type bool $routeToLeader Enable/disable Leader Aware Routing. + * **Defaults to** `true` (enabled). + * @type array $defaultQueryOptions + * @type SessionPoolInterface $sessionPool The session pool + * implementation. + * @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. + * @type string $databaseRole The user created database role which + * creates the session. + * @type array $database The database info. * @type int $isolationLevel The IsolationLevel set for the transaction. * Check {@see IsolationLevel} for more details. * } * @return Database */ - public function database($name, array $options = []) + public function database(string $name, array $options = []): Database { return new Database( - $this->connection, + $this->spannerClient, + $this->databaseAdminClient, + $this->serializer, $this, - $this->lroConnection, - $this->lroCallables, $this->projectId, $name, - isset($options['sessionPool']) ? $options['sessionPool'] : null, - $this->returnInt64AsObject, - isset($options['database']) ? $options['database'] : [], - isset($options['databaseRole']) ? $options['databaseRole'] : '', - $options['isolationLevel'] ?? $this->isolationLevel, + $options + [ + 'routeToLeader' => $this->routeToLeader, + 'defaultQueryOptions' => $this->defaultQueryOptions, + 'returnInt64AsObject' => $this->returnInt64AsObject, + 'isolationLevel' => $this->isolationLevel, + ] ); } @@ -570,21 +572,28 @@ public function database($name, array $options = []) * } * @return ItemIterator */ - public function databases(array $options = []) + public function databases(array $options = []): ItemIterator { - $resultLimit = $this->pluck('resultLimit', $options, false); - return new ItemIterator( - new PageIterator( - function (array $database) { - return $this->database($database['name'], ['database' => $database]); - }, - [$this->connection, 'listDatabases'], - $options + ['instance' => $this->name], - [ - 'itemsKey' => 'databases', - 'resultLimit' => $resultLimit - ] - ) + /** + * @var ListDatabasesRequest $listDatabasesRequest + * @var array $callOptions + */ + [$listDatabasesRequest, $callOptions] = $this->validateOptions( + $options, + new ListDatabasesRequest(), + CallOptions::class + ); + $listDatabasesRequest->setParent($this->name); + + return $this->buildListItemsIterator( + [$this->databaseAdminClient, 'listDatabases'], + $listDatabasesRequest, + $callOptions + ['resource-prefix' => $this->name], + function (array $database) { + return $this->database($database['name'], ['database' => $database]); + }, + 'databases', + $this->pluck('resultLimit', $options, false) ); } @@ -600,16 +609,15 @@ function (array $database) { * * @return Backup */ - public function backup($name, array $backup = []) + public function backup(string $name, array $backup = []): Backup { return new Backup( - $this->connection, + $this->databaseAdminClient, + $this->serializer, $this, - $this->lroConnection, - $this->lroCallables, $this->projectId, $name, - $backup + ['backup' => $backup] ); } @@ -644,24 +652,28 @@ public function backup($name, array $backup = []) * * @return ItemIterator */ - public function backups(array $options = []) + public function backups(array $options = []): ItemIterator { - $resultLimit = $this->pluck('resultLimit', $options, false); - return new ItemIterator( - new PageIterator( - function (array $backup) { - return $this->backup( - $backup['name'], - $backup - ); - }, - [$this->connection, 'listBackups'], - $options + ['instance' => $this->name], - [ - 'itemsKey' => 'backups', - 'resultLimit' => $resultLimit - ] - ) + /** + * @var ListBackupsRequest $listBackupsRequest + * @var array $callOptions + */ + [$listBackupsRequest, $callOptions] = $this->validateOptions( + $options, + new ListBackupsRequest(), + CallOptions::class + ); + $listBackupsRequest->setParent($this->name); + + return $this->buildListItemsIterator( + [$this->databaseAdminClient, 'listBackups'], + $listBackupsRequest, + $callOptions + ['resource-prefix' => $this->name], + function (array $backup) { + return $this->backup($backup['name'], $backup); + }, + 'backups', + $this->pluck('resultLimit', $options, false) ); } @@ -691,22 +703,9 @@ function (array $backup) { * * @return ItemIterator */ - public function backupOperations(array $options = []) + public function backupOperations(array $options = []): ItemIterator { - $resultLimit = $this->pluck('resultLimit', $options, false); - return new ItemIterator( - new PageIterator( - function (array $operation) { - return $this->resumeOperation($operation['name'], $operation); - }, - [$this->connection, 'listBackupOperations'], - $options + ['instance' => $this->name], - [ - 'itemsKey' => 'operations', - 'resultLimit' => $resultLimit - ] - ) - ); + return $this->database($this->name)->backupOperations($options); } /** @@ -735,22 +734,9 @@ function (array $operation) { * * @return ItemIterator */ - public function databaseOperations(array $options = []) + public function databaseOperations(array $options = []): ItemIterator { - $resultLimit = $this->pluck('resultLimit', $options, false); - return new ItemIterator( - new PageIterator( - function (array $operation) { - return $this->resumeOperation($operation['name'], $operation); - }, - [$this->connection, 'listDatabaseOperations'], - $options + ['instance' => $this->name], - [ - 'itemsKey' => 'operations', - 'resultLimit' => $resultLimit - ] - ) - ); + return $this->database($this->name)->databaseOperations($options); } /** @@ -761,13 +747,15 @@ function (array $operation) { * $iam = $instance->iam(); * ``` * - * @return Iam + * @return IamManager */ - public function iam() + public function iam(): IamManager { if (!$this->iam) { - $this->iam = new Iam( - new IamInstance($this->connection), + $this->iam = new IamManager( + new RequestHandler($this->serializer, [$this->instanceAdminClient]), + $this->serializer, + InstanceAdminClient::class, $this->name ); } @@ -782,31 +770,12 @@ public function iam() * @param string $project The project ID. * @return string */ - private function fullyQualifiedInstanceName($name, $project) + private function fullyQualifiedInstanceName(string $name, string $project): string { - // try { return InstanceAdminClient::instanceName( $project, $name ); - // } catch (ValidationException $e) { - // return $name; - // } - } - - /** - * Extracts a database id from fully qualified name. - * - * @param string $name The database name or id. - * @return string - */ - private function databaseIdOnly($name) - { - try { - return DatabaseAdminClient::parseName($name)['database']; - } catch (ValidationException $e) { - return $name; - } } /** @@ -818,7 +787,9 @@ private function databaseIdOnly($name) public function __debugInfo() { return [ - 'connection' => get_class($this->connection), + 'spannerClient' => get_class($this->spannerClient), + 'databaseAdminClient' => get_class($this->databaseAdminClient), + 'instanceAdminClient' => get_class($this->instanceAdminClient), 'projectId' => $this->projectId, 'name' => $this->name, 'info' => $this->info @@ -835,8 +806,138 @@ public function __debugInfo() * * @return array */ - public function directedReadOptions() + public function directedReadOptions(): array { return $this->directedReadOptions; } + + /** + * @param array $instanceArray + * @return array + */ + private function fieldMask(array $instanceArray): array + { + $mask = []; + foreach (array_keys($instanceArray) as $key) { + $mask[] = $this->serializer::toSnakeCase($key); + } + return ['paths' => $mask]; + } + + /** + * @param array $instanceArray + * @param InstanceConfiguration $config + * @return array + */ + public function createInstanceArray( + array $instanceArray, + InstanceConfiguration|null $config = null + ): array { + return $instanceArray + [ + 'name' => $this->name, + 'displayName' => InstanceAdminClient::parseName($this->name)['instance'], + 'labels' => [], + 'config' => $config ? $config->name() : '' + ]; + } + + /** + * Resume a Long Running Operation + * + * Example: + * ``` + * $operation = $instance->resumeOperation($operationName); + * ``` + * + * @param string $operationName The Long Running Operation name. + * @return LongRunningOperation + */ + public function resumeOperation(string $operationName, array $options = []): LongRunningOperation + { + return new LongRunningOperation( + new LongRunningClientConnection($this->instanceAdminClient, $this->serializer), + $operationName, + [ + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata', + 'callable' => $this->instanceResultFunction() + ], + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', + 'callable' => $this->instanceResultFunction() + ] + ], + $options + ); + } + + /** + * 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 + */ + public function longRunningOperations(array $options = []): ItemIterator + { + /** + * @var ListOperationsRequest $listOperationsRequest + * @var array $callOptions + */ + [$listOperationsRequest, $callOptions] = $this->validateOptions( + $options, + new ListOperationsRequest(), + CallOptions::class + ); + $listOperationsRequest->setName($this->name . '/operations'); + + return $this->buildLongRunningIterator( + [$this->instanceAdminClient->getOperationsClient(), 'listOperations'], + $listOperationsRequest, + $callOptions, + function (OperationProto $operation) { + return $this->resumeOperation( + $operation->getName(), + $this->handleResponse($operation) + ); + } + ); + } + + private function instanceResultFunction(): Closure + { + return function (array $result) { + $name = InstanceAdminClient::parseName($result['name']); + return new self( + $this->spannerClient, + $this->instanceAdminClient, + $this->databaseAdminClient, + $this->serializer, + $this->projectId, + $name['instance'], + [ + 'directedReadOptions' => $this->directedReadOptions, + 'routeToLeader' => $this->routeToLeader, + 'defaultQueryOptions' => $this->defaultQueryOptions, + 'returnInt64AsObject' => $this->returnInt64AsObject, + 'instance' => $result, + ], + ); + }; + } } diff --git a/Spanner/src/InstanceConfiguration.php b/Spanner/src/InstanceConfiguration.php index da6d6fbb4f22..2c0f35d66763 100644 --- a/Spanner/src/InstanceConfiguration.php +++ b/Spanner/src/InstanceConfiguration.php @@ -17,19 +17,22 @@ namespace Google\Cloud\Spanner; +use Closure; +use Google\ApiCore\ApiException; +use Google\ApiCore\Options\CallOptions; use Google\ApiCore\ValidationException; -use Google\Cloud\Core\ArrayTrait; -use Google\Cloud\Core\Exception\NotFoundException; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\LongRunning\LongRunningClientConnection; use Google\Cloud\Core\LongRunning\LongRunningOperation; -use Google\Cloud\Core\LongRunning\LROTrait; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Core\OptionsValidator; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\CreateInstanceConfigRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\DeleteInstanceConfigRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\GetInstanceConfigRequest; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig\State; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig\Type; use Google\Cloud\Spanner\Admin\Instance\V1\ReplicaInfo; -use Google\Cloud\Spanner\Connection\ConnectionInterface; -use Google\Cloud\Spanner\Connection\LongRunningConnection; +use Google\Cloud\Spanner\Admin\Instance\V1\UpdateInstanceConfigRequest; +use Google\Rpc\Code; /** * Represents a Cloud Spanner Instance Configuration. @@ -38,7 +41,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => $projectId]); * * $configuration = $spanner->instanceConfiguration('regional-europe-west'); * ``` @@ -49,78 +52,35 @@ */ class InstanceConfiguration { - use ArrayTrait; - use LROTrait; + use RequestTrait; - /** - * @var ConnectionInterface - * @internal - */ - private $connection; - - /** - * @var string - */ - private $projectId; - - /** - * @var string - */ - private $name; - - /** - * @var array - */ - private $info; + private array $info; /** * Create an instance configuration object. * - * @param ConnectionInterface $connection A service connection for the - * Spanner API. This object is created by SpannerClient, - * and should not be instantiated outside of this client. + * @internal InstanceConfiguration is constructed by the {@see SpannerClient} class. + * + * @param InstanceAdminClient $instanceAdminClient The client library to use for the request + * @param Serializer $serializer The serializer instance to encode/decode messages. * @param string $projectId The current project ID. * @param string $name The configuration name or ID. - * @param array $info [optional] A service representation of the - * configuration. - * @param LongRunningConnectionInterface $lroConnection An implementation - * mapping to methods which handle LRO resolution in the service. + * @param array $options [Optional] { + * Instance Configuration options. + + * @type array $instanceConfig The instance configuration info. + * } */ public function __construct( - ConnectionInterface $connection, - $projectId, - $name, - array $info = [], - ?LongRunningConnectionInterface $lroConnection = null + private InstanceAdminClient $instanceAdminClient, + private Serializer $serializer, + private string $projectId, + private string $name, + private array $options = [], ) { - $this->connection = $connection; - $this->projectId = $projectId; $this->name = $this->fullyQualifiedConfigName($name, $projectId); - $this->info = $info; - $lroConnection = $lroConnection ?: new LongRunningConnection($this->connection); - $instanceConfigFactoryFn = function ($instanceConfig) use ($connection, $projectId, $name, $lroConnection) { - $name = InstanceAdminClient::parseName($instanceConfig['name'])['instance_config']; - return new self( - $connection, - $projectId, - $name, - $instanceConfig, - $lroConnection - ); - }; - $this->setLroProperties( - $lroConnection, - [ - [ - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata', - 'callable' => $instanceConfigFactoryFn - ], - [ - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata', - 'callable' => $instanceConfigFactoryFn - ] - ] - ); + $this->info = $options['instanceConfig'] ?? []; + $this->optionsValidator = new OptionsValidator($serializer); } /** @@ -185,8 +145,11 @@ public function exists(array $options = []) { try { $this->reload($options = []); - } catch (NotFoundException $e) { - return false; + } catch (ApiException $e) { + if ($e->getCode() === Code::NOT_FOUND) { + return false; + } + throw $e; } return true; @@ -209,12 +172,22 @@ public function exists(array $options = []) */ public function reload(array $options = []) { - $this->info = $this->connection->getInstanceConfig($options + [ - 'name' => $this->name, - 'projectName' => InstanceAdminClient::projectName($this->projectId), + $options += ['name' => $this->name]; + /** + * @var GetInstanceConfigRequest $getInstanceConfig + * @var array $callOptions + */ + [$getInstanceConfig, $callOptions] = $this->validateOptions( + $options, + new GetInstanceConfigRequest(), + CallOptions::class + ); + + $response = $this->instanceAdminClient->getInstanceConfig($getInstanceConfig, $callOptions + [ + 'resource-prefix' => InstanceAdminClient::projectName($this->projectId), ]); - return $this->info; + return $this->info = $this->handleResponse($response); } /** @@ -247,35 +220,41 @@ public function reload(array $options = []) * @type bool $validateOnly An option to validate, but not actually execute, the request, and provide the same * response. **Defaults to** `false`. * } - * @return LongRunningOperation + * @return LongRunningOperation * @throws ValidationException * @codingStandardsIgnoreEnd */ public function create(InstanceConfiguration $baseConfig, array $replicas, array $options = []) { - $configId = InstanceAdminClient::parseName($this->name)['instance_config']; + [$data, $callOptions] = $this->splitOptionalArgs($options); + $leaderOptions = $baseConfig->__debugInfo()['info']['leaderOptions'] ?? []; - $options += [ - 'displayName' => $configId, - 'labels' => [], + $validateOnly = $data['validateOnly'] ?? false; + unset($data['validateOnly']); + $data += [ 'replicas' => $replicas, - 'leaderOptions' => $leaderOptions, + 'baseConfig' => $baseConfig->name(), + 'leaderOptions' => $leaderOptions + ]; + $instanceConfig = $this->instanceConfigArray($data); + $requestArray = [ + 'parent' => InstanceAdminClient::projectName($this->projectId), + 'instanceConfigId' => InstanceAdminClient::parseName($this->name)['instance_config'], + 'instanceConfig' => $instanceConfig, + 'validateOnly' => $validateOnly ]; - // Set output parameters to their default values. - $options['state'] = State::CREATING; - $options['configType'] = Type::USER_MANAGED; - $options['optionalReplicas'] = []; - $options['reconciling'] = false; + $request = $this->serializer->decodeMessage( + new CreateInstanceConfigRequest(), + $requestArray + ); - $operation = $this->connection->createInstanceConfig([ - 'instanceConfigId' => $configId, - 'name' => $this->name, - 'projectName' => InstanceAdminClient::projectName($this->projectId), - 'baseConfig' => $baseConfig->name(), - ] + $options); + $operation = $this->instanceAdminClient->createInstanceConfig( + $request, + $callOptions + ['resource-prefix' => $this->name] + ); - return $this->resumeOperation($operation['name'], $operation); + return $this->operationFromOperationResponse($operation); } /** @@ -302,16 +281,27 @@ public function create(InstanceConfiguration $baseConfig, array $replicas, array * @type bool $validateOnly An option to validate, but not actually execute, the request, and provide the same * response. **Defaults to** `false`. * } - * @return LongRunningOperation + * @return LongRunningOperation * @throws \InvalidArgumentException */ public function update(array $options = []) { - $operation = $this->connection->updateInstanceConfig([ - 'name' => $this->name, - ] + $options); + [$data, $callOptions] = $this->splitOptionalArgs($options); + $validateOnly = $data['validateOnly'] ?? false; + unset($data['validateOnly']); - return $this->resumeOperation($operation['name'], $operation); + $request = $this->serializer->decodeMessage(new UpdateInstanceConfigRequest(), [ + 'instanceConfig' => $data + ['name' => $this->name], + 'updateMask' => $this->fieldMask($data), + 'validateOnly' => $validateOnly + ]); + + $operation = $this->instanceAdminClient->updateInstanceConfig( + $request, + $callOptions + ['resource-prefix' => $this->name] + ); + + return $this->operationFromOperationResponse($operation); } /** @@ -332,25 +322,51 @@ public function update(array $options = []) */ public function delete(array $options = []) { - $this->connection->deleteInstanceConfig([ - 'name' => $this->name - ] + $options); + $options += ['name' => $this->name]; + + /** + * @var DeleteInstanceConfigRequest $deleteInstanceConfigs + * @var array $callOptions + */ + [$deleteInstanceConfigs, $callOptions] = $this->validateOptions( + $options, + new DeleteInstanceConfigRequest(), + CallOptions::class + ); + + $this->instanceAdminClient->deleteInstanceConfig($deleteInstanceConfigs, $callOptions + [ + 'resource-prefix' => $this->name + ]); } /** - * A more readable representation of the object. + * Resume a Long Running Operation * - * @codeCoverageIgnore - * @access private + * Example: + * ``` + * $operation = $spanner->resumeOperation($operationName); + * ``` + * + * @param string $operationName The Long Running Operation name. + * @return LongRunningOperation */ - public function __debugInfo() + public function resumeOperation(string $operationName, array $options = []) { - return [ - 'connection' => get_class($this->connection), - 'projectId' => $this->projectId, - 'name' => $this->name, - 'info' => $this->info, - ]; + return new LongRunningOperation( + new LongRunningClientConnection($this->instanceAdminClient, $this->serializer), + $operationName, + [ + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceConfigMetadata', + 'callable' => $this->instanceConfigResultFunction(), + ], + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceConfigMetadata', + 'callable' => $this->instanceConfigResultFunction(), + ] + ], + $options + ); } /** @@ -371,4 +387,63 @@ private function fullyQualifiedConfigName($name, $projectId) return $name; } } + + /** + * @param array $args + * + * @return array + */ + private function instanceConfigArray(array $args) + { + $configId = InstanceAdminClient::parseName($this->name)['instance_config']; + + return $args += [ + 'name' => $this->name, + 'displayName' => $configId, + 'configType' => Type::USER_MANAGED + ]; + } + + /** + * @param array $instanceArray + * @return array + */ + private function fieldMask(array $instanceArray) + { + $mask = []; + foreach (array_keys($instanceArray) as $key) { + $mask[] = $this->serializer::toSnakeCase($key); + } + return ['paths' => $mask]; + } + + private function instanceConfigResultFunction(): Closure + { + return function (array $result) { + $name = InstanceAdminClient::parseName($result['name']); + return new self( + $this->instanceAdminClient, + $this->serializer, + $this->projectId, + $name['instance_config'], + ['instanceConfig' => $result], + ); + }; + } + + /** + * A more readable representation of the object. + * + * @codeCoverageIgnore + * @access private + */ + public function __debugInfo() + { + return [ + 'instanceAdminClient' => get_class($this->instanceAdminClient), + 'projectId' => $this->projectId, + 'name' => $this->name, + 'info' => $this->info, + ]; + } } diff --git a/Spanner/src/KeyRange.php b/Spanner/src/KeyRange.php index 727d33819191..9f402a1f4744 100644 --- a/Spanner/src/KeyRange.php +++ b/Spanner/src/KeyRange.php @@ -24,7 +24,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * // Create a KeyRange for all people named Bob, born in 1969. * $start = $spanner->date(new \DateTime('1969-01-01')); @@ -44,31 +44,7 @@ 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 = [ + private const DEFINITION = [ self::TYPE_OPEN => [ 'start' => 'startOpen', 'end' => 'endOpen' @@ -79,6 +55,11 @@ class KeyRange ] ]; + private string $startType; + private array|null $start; + private string $endType; + private array|null $end; + /** * Create a KeyRange. * @@ -130,7 +111,7 @@ public function __construct(array $options = []) * @param array $key The key to match against. * @return KeyRange */ - public static function prefixMatch(array $key) + public static function prefixMatch(array $key): KeyRange { return new static([ 'startType' => self::TYPE_CLOSED, @@ -150,7 +131,7 @@ public static function prefixMatch(array $key) * * @return array|null */ - public function start() + public function start(): array|null { return $this->start; } @@ -169,7 +150,7 @@ public function start() * @param array $start The start of the key range. * @return void */ - public function setStart($type, array $start) + public function setStart(string $type, array $start): void { $rangeKey = $this->fromDefinition($type, 'start'); @@ -187,7 +168,7 @@ public function setStart($type, array $start) * * @return array */ - public function end() + public function end(): array { return $this->end; } @@ -206,12 +187,12 @@ public function end() * @param array $end The end of the key range. * @return void */ - public function setEnd($type, array $end) + public function setEnd(string $type, array $end): void { - if (!in_array($type, array_keys($this->definition))) { + if (!in_array($type, array_keys(self::DEFINITION))) { throw new \InvalidArgumentException(sprintf( 'Invalid KeyRange type. Allowed values are %s', - implode(', ', array_keys($this->definition)) + implode(', ', array_keys(self::DEFINITION)) )); } @@ -245,9 +226,9 @@ public function types() * @return array * @access private */ - public function keyRangeObject() + public function keyRangeObject(): array { - if (!$this->start || !$this->end) { + if (!isset($this->start) || !isset($this->end)) { throw new \BadMethodCallException('Key Range must supply a start and an end'); } @@ -264,7 +245,7 @@ public function keyRangeObject() * @return KeyRange * @access private */ - public static function fromArray(array $range) + public static function fromArray(array $range): KeyRange { $startType = null; $start = null; @@ -301,16 +282,16 @@ public static function fromArray(array $range) * @param mixed $startOrEnd * @return string */ - private function fromDefinition($type, $startOrEnd) + private function fromDefinition(string $type, mixed $startOrEnd): string { - if (!array_key_exists($type, $this->definition)) { + if (!array_key_exists($type, self::DEFINITION)) { throw new \InvalidArgumentException(sprintf( 'Invalid KeyRange %s type. Allowed values are %s.', $startOrEnd, - implode(', ', array_keys($this->definition)) + implode(', ', array_keys(self::DEFINITION)) )); } - return $this->definition[$type][$startOrEnd]; + return self::DEFINITION[$type][$startOrEnd]; } } diff --git a/Spanner/src/KeySet.php b/Spanner/src/KeySet.php index f1573f7971a0..7d40eaa484d4 100644 --- a/Spanner/src/KeySet.php +++ b/Spanner/src/KeySet.php @@ -26,7 +26,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $keySet = $spanner->keySet(); * ``` @@ -37,20 +37,12 @@ class KeySet { use ValidateTrait; - /** - * @var array - */ - private $keys; - + private array $keys; /** * @var KeyRange[] */ - private $ranges; - - /** - * @var bool - */ - private $all; + private array $ranges; + private bool $all; /** * Create a KeySet. @@ -97,7 +89,7 @@ public function __construct(array $options = []) * * @return KeyRange[] */ - public function ranges() + public function ranges(): array { return $this->ranges; } @@ -114,7 +106,7 @@ public function ranges() * @param KeyRange $range A KeyRange instance. * @return void */ - public function addRange(KeyRange $range) + public function addRange(KeyRange $range): void { $this->ranges[] = $range; } @@ -133,7 +125,7 @@ public function addRange(KeyRange $range) * @param KeyRange[] $ranges An array of KeyRange objects. * @return void */ - public function setRanges(array $ranges) + public function setRanges(array $ranges): void { $this->validateBatch($ranges, KeyRange::class); @@ -150,7 +142,7 @@ public function setRanges(array $ranges) * * @return mixed[] */ - public function keys() + public function keys(): array { return $this->keys; } @@ -169,7 +161,7 @@ public function keys() * @param mixed $key The Key to add. * @return void */ - public function addKey($key) + public function addKey($key): void { $this->keys[] = $key; } @@ -187,7 +179,7 @@ public function addKey($key) * @param mixed[] $keys * @return void */ - public function setKeys(array $keys) + public function setKeys(array $keys): void { $this->keys = $keys; } @@ -204,7 +196,7 @@ public function setKeys(array $keys) * * @return bool */ - public function matchAll() + public function matchAll(): bool { return $this->all; } @@ -220,7 +212,7 @@ public function matchAll() * @param bool $all If true, all keys in a table will be matched. * @return void */ - public function setMatchAll($all) + public function setMatchAll($all): void { $this->all = (bool) $all; } @@ -230,7 +222,7 @@ public function setMatchAll($all) * * @access private */ - public function keySetObject() + public function keySetObject(): array { $ranges = []; foreach ($this->ranges as $range) { @@ -261,7 +253,7 @@ public function keySetObject() * @return KeySet * @access private */ - public static function fromArray(array $keySet) + public static function fromArray(array $keySet): KeySet { $res = new self(); if (isset($keySet['all'])) { diff --git a/Spanner/src/Middleware/SpannerMiddleware.php b/Spanner/src/Middleware/SpannerMiddleware.php new file mode 100644 index 000000000000..7d602a54d14b --- /dev/null +++ b/Spanner/src/Middleware/SpannerMiddleware.php @@ -0,0 +1,109 @@ +nextHandler = $nextHandler; + $this->serializer = new Serializer(); + } + + /** + * @param Call $call + * @param array $options + * + * @return PromiseInterface|ClientStream|ServerStream|BidiStream + * @throws Throwable + */ + public function __invoke( + Call $call, + array $options + ): PromiseInterface|ClientStream|ServerStream|BidiStream { + if ($resourcePrefix = $this->pluck('resource-prefix', $options, false)) { + $options['headers'][self::RESOURCE_PREFIX_HEADER] = [$options['resource-prefix']]; + } + + if (true === $this->pluck('route-to-leader', $options, false)) { + $options['headers'][self::ROUTE_TO_LEADER_HEADER] = ['true']; + } + + $response = ($this->nextHandler)($call, $options); + if ($response instanceof PromiseInterface) { + return $response->then(null, function ($value) { + if ($value instanceof ApiException) { + throw $this->convertToGoogleException($value); + } + if ($value instanceof Throwable) { + throw $value; + } + }); + } + + // this can also be a Stream + return $response; + } +} diff --git a/Spanner/src/MutationTrait.php b/Spanner/src/MutationTrait.php index 0fd3b5b12728..d390e71821e2 100644 --- a/Spanner/src/MutationTrait.php +++ b/Spanner/src/MutationTrait.php @@ -17,8 +17,6 @@ namespace Google\Cloud\Spanner; -use Google\Cloud\Core\ArrayTrait; - /** * Common helper methods used for creating array representation of * {@see \Google\Cloud\Spanner\V1\Mutation} @@ -27,11 +25,6 @@ */ trait MutationTrait { - use ArrayTrait; - - /** - * @var array - */ private array $mutationData = []; /** @@ -48,9 +41,9 @@ trait MutationTrait * * @param string $table The table to insert into. * @param array $data The data to insert. - * @return MutationGroup Current mutation group data object. + * @return self Current mutation group data object. */ - public function insert($table, array $data) + public function insert(string $table, array $data): self { return $this->insertBatch($table, [$data]); } @@ -73,7 +66,7 @@ public function insert($table, array $data) * @param array $dataSet The data to insert. * @return $this Current object instance, to enable object chaining. */ - public function insertBatch($table, array $dataSet) + public function insertBatch(string $table, array $dataSet): self { $this->enqueue(Operation::OP_INSERT, $table, $dataSet); @@ -96,7 +89,7 @@ public function insertBatch($table, array $dataSet) * @param array $data The data to update. * @return $this Current object instance, to enable object chaining. */ - public function update($table, array $data) + public function update(string $table, array $data): self { return $this->updateBatch($table, [$data]); } @@ -119,7 +112,7 @@ public function update($table, array $data) * @param array $dataSet The data to update. * @return $this Current object instance, to enable object chaining. */ - public function updateBatch($table, array $dataSet) + public function updateBatch(string $table, array $dataSet): self { $this->enqueue(Operation::OP_UPDATE, $table, $dataSet); @@ -142,7 +135,7 @@ public function updateBatch($table, array $dataSet) * @param array $data The data to insert or update. * @return $this Current mutation group, to enable object chaining. */ - public function insertOrUpdate($table, array $data) + public function insertOrUpdate(string $table, array $data): self { return $this->insertOrUpdateBatch($table, [$data]); } @@ -165,7 +158,7 @@ public function insertOrUpdate($table, array $data) * @param array $dataSet The data to insert or update. * @return $this Current object instance, to enable object chaining. */ - public function insertOrUpdateBatch($table, array $dataSet) + public function insertOrUpdateBatch(string $table, array $dataSet): self { $this->enqueue(Operation::OP_INSERT_OR_UPDATE, $table, $dataSet); @@ -188,7 +181,7 @@ public function insertOrUpdateBatch($table, array $dataSet) * @param array $data The data to replace. * @return $this Current object instance, to enable object chaining. */ - public function replace($table, array $data) + public function replace(string $table, array $data): self { return $this->replaceBatch($table, [$data]); } @@ -211,7 +204,7 @@ public function replace($table, array $data) * @param array $dataSet The data to replace. * @return $this Current object instance, to enable object chaining. */ - public function replaceBatch($table, array $dataSet) + public function replaceBatch(string $table, array $dataSet): self { $this->enqueue(Operation::OP_REPLACE, $table, $dataSet); @@ -234,7 +227,7 @@ public function replaceBatch($table, array $dataSet) * @param KeySet $keySet The KeySet to identify rows to delete. * @return $this Current object instance, to enable object chaining. */ - public function delete($table, KeySet $keySet) + public function delete(string $table, KeySet $keySet): self { $this->enqueue(Operation::OP_DELETE, $table, [$keySet]); @@ -249,7 +242,7 @@ public function delete($table, KeySet $keySet) * @param array $dataSet the mutations to enqueue * @return void */ - private function enqueue($op, $table, array $dataSet) + private function enqueue(string $op, string $table, array $dataSet): void { foreach ($dataSet as $data) { if ($op === Operation::OP_DELETE) { @@ -269,7 +262,7 @@ private function enqueue($op, $table, array $dataSet) * key/value pairs. * @return array */ - public function mutation($operation, $table, $mutation) + public function mutation(string $operation, string $table, array $mutation): array { return [ $operation => [ @@ -287,7 +280,7 @@ public function mutation($operation, $table, $mutation) * @param KeySet $keySet The keys to delete. * @return array */ - public function deleteMutation($table, KeySet $keySet) + public function deleteMutation(string $table, KeySet $keySet): array { return [ 'delete' => [ @@ -301,12 +294,12 @@ public function deleteMutation($table, KeySet $keySet) * Returns the mutation data as associative array. * @return array */ - private function getMutations() + private function getMutations(): array { return $this->mutationData; } - private function getValueMapper() + private function getValueMapper(): ValueMapper { if (!isset($this->mapper)) { $this->mapper = new ValueMapper(false); @@ -315,7 +308,7 @@ private function getValueMapper() return $this->mapper; } - private function flattenKeySet(KeySet $keySet) + private function flattenKeySet(KeySet $keySet): array { $keys = $keySet->keySetObject(); @@ -333,6 +326,6 @@ private function flattenKeySet(KeySet $keySet) $keys['keys'] = $this->getValueMapper()->encodeValuesAsSimpleType($keys['keys'], true); } - return $this->arrayFilterRemoveNull($keys); + return array_filter($keys, fn ($v) => !is_null($v)); } } diff --git a/Spanner/src/Numeric.php b/Spanner/src/Numeric.php index 63d3a47ab170..d2510611a5ae 100644 --- a/Spanner/src/Numeric.php +++ b/Spanner/src/Numeric.php @@ -29,7 +29,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $numeric = $spanner->numeric('99999999999999999999999999999999999999.999999999'); * ``` @@ -67,7 +67,7 @@ public function __construct($value) * * @return string */ - public function get() + public function get(): string { return $this->value; } @@ -75,9 +75,9 @@ public function get() /** * Get the type. * - * @return string + * @return int */ - public function type() + public function type(): int { return ValueMapper::TYPE_NUMERIC; } @@ -87,7 +87,7 @@ public function type() * * @return string */ - public function formatAsString() + public function formatAsString(): string { return $this->value; } diff --git a/Spanner/src/Operation.php b/Spanner/src/Operation.php index 043d504bb6b5..28d1d807f288 100644 --- a/Spanner/src/Operation.php +++ b/Spanner/src/Operation.php @@ -17,15 +17,32 @@ namespace Google\Cloud\Spanner; -use Google\Cloud\Core\ArrayTrait; -use Google\Cloud\Core\TimeTrait; -use Google\Cloud\Core\ValidateTrait; +use Generator; +use Google\ApiCore\Options\CallOptions; +use Google\Cloud\Core\ApiHelperTrait; +use Google\Cloud\Core\OptionsValidator; +use Google\Cloud\Core\RequestProcessorTrait; use Google\Cloud\Spanner\Batch\QueryPartition; use Google\Cloud\Spanner\Batch\ReadPartition; -use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Session\Session; -use Google\Cloud\Spanner\V1\SpannerClient as GapicSpannerClient; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CommitRequest; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlRequest; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\PartitionOptions; +use Google\Cloud\Spanner\V1\PartitionQueryRequest; +use Google\Cloud\Spanner\V1\PartitionReadRequest; +use Google\Cloud\Spanner\V1\ReadRequest; +use Google\Cloud\Spanner\V1\RequestOptions; +use Google\Cloud\Spanner\V1\RollbackRequest; +use Google\Cloud\Spanner\V1\TransactionOptions; +use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite; +use Google\Cloud\Spanner\V1\TransactionSelector; +use Google\Cloud\Spanner\V1\Type; use Google\Rpc\Code; +use GPBMetadata\Google\Protobuf\Struct; use InvalidArgumentException; /** @@ -40,10 +57,9 @@ */ class Operation { - use ArrayTrait; + use ApiHelperTrait; + use RequestProcessorTrait; use MutationTrait; - use TimeTrait; - use ValidateTrait; const OP_INSERT = 'insert'; const OP_UPDATE = 'update'; @@ -51,29 +67,38 @@ class Operation const OP_REPLACE = 'replace'; const OP_DELETE = 'delete'; - /** - * @var ConnectionInterface - * @internal - */ - private $connection; - - /** - * @var ValueMapper - */ - private $mapper; + private ValueMapper $mapper; + private bool $routeToLeader; + private array $defaultQueryOptions; /** - * @param ConnectionInterface $connection A connection to Google Cloud - * Spanner. This object is created by SpannerClient, - * and should not be instantiated outside of this client. - * @param bool $returnInt64AsObject If true, 64 bit integers will be + * @param SpannerClient $spannerClient The Spanner client used to make requests. + * @param Serializer $serializer The serializer instance to encode/decode messages. + * @param array $options { + * Configuration options. + * + * @type bool $routeToLeader Enable/disable Leader Aware Routing. + * **Defaults to** `true` (enabled). + * @type array $defaultQueryOptions + * @type bool $returnInt64AsObject If true, 64 bit integers will be * returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit * platform compatibility. + * } */ - public function __construct(ConnectionInterface $connection, $returnInt64AsObject) - { - $this->connection = $connection; - $this->mapper = new ValueMapper($returnInt64AsObject); + public function __construct( + private SpannerClient $spannerClient, + private Serializer $serializer, + array $options = [] + ) { + /** @var array $options */ + $options = $this->validateOptions( + $options, + ['returnInt64AsObject', 'routeToLeader', 'defaultQueryOptions'] + ); + $this->mapper = new ValueMapper($options['returnInt64AsObject'] ?? false); + $this->routeToLeader = $options['routeToLeader'] ?? true; + $this->defaultQueryOptions = $options['defaultQueryOptions'] ?? []; + $this->optionsValidator = new OptionsValidator($serializer); } /** @@ -99,7 +124,7 @@ public function __construct(ConnectionInterface $connection, $returnInt64AsObjec * } * @return Timestamp The commit Timestamp. */ - public function commit(Session $session, array $mutations, array $options = []) + public function commit(Session $session, array $mutations, array $options = []): Timestamp { return $this->commitWithResponse($session, $mutations, $options)[0]; } @@ -130,20 +155,48 @@ public function commit(Session $session, array $mutations, array $options = []) * @return array An array containing {@see \Google\Cloud\Spanner\Timestamp} * at index 0 and the commit response as an array at index 1. */ - public function commitWithResponse(Session $session, array $mutations, array $options = []) + public function commitWithResponse(Session $session, array $mutations, array $options = []): array { $options += [ - 'transactionId' => null + 'session' => $session->name(), + 'mutations' => $this->serializeMutations($mutations), ]; + /** + * @TODO: Find out why singleUse is being passed in and if we can remove it. + * + * @var CommitRequest $commitRequest + * @var bool|null $_singleUse + * @var array $callOptions + */ + [$commitRequest, $_singleUse, $callOptions] = $this->validateOptions( + $options, + new CommitRequest(), + 'singleUse', // Internal flag, need to unset before passing to serializer + CallOptions::class + ); - $res = $this->connection->commit($this->arrayFilterRemoveNull([ - 'mutations' => $mutations, - 'session' => $session->name(), - 'database' => $this->getDatabaseNameFromSession($session) - ]) + $options); + // Configure Single Use Transaction options + // @TODO: Find out why we do this + if ($commitRequest->hasSingleUseTransaction()) { + // This will clear any Transaction ID (because its a oneof) + $commitRequest->setSingleUseTransaction( + (new TransactionOptions())->setReadWrite(new ReadWrite()) + ); + } - $time = $this->parseTimeString($res['commitTimestamp']); - return [new Timestamp($time[0], $time[1]), $res]; + $response = $this->spannerClient->commit($commitRequest, $callOptions + [ + 'resource-prefix' => $this->getDatabaseNameFromSession($session), + 'route-to-leader' => $this->routeToLeader + ]); + $timestamp = $response->getCommitTimestamp(); + + return [ + new Timestamp( + $this->createDateTimeFromSeconds($timestamp->getSeconds()), + $timestamp->getNanos() + ), + $this->handleResponse($response) + ]; } /** @@ -157,16 +210,34 @@ public function commitWithResponse(Session $session, array $mutations, array $op * @return void * @throws InvalidArgumentException If the transaction is not yet initialized. */ - public function rollback(Session $session, $transactionId, array $options = []) - { + public function rollback( + Session $session, + string|null $transactionId, + array $options = [] + ): void { if (empty($transactionId)) { throw new InvalidArgumentException('Rollback failed: Transaction not initiated.'); } - $this->connection->rollback([ - 'transactionId' => $transactionId, - 'session' => $session->name(), - 'database' => $this->getDatabaseNameFromSession($session) - ] + $options); + + /** + * @TODO: find out why "transactionOptions" are being passed in by are unused. + * + * @var array $callOptions + * @var array|null $_transactionOptions + */ + [$callOptions, $_transactionOptions] = $this->validateOptions( + $options, + CallOptions::class, + 'transactionOptions' + ); + $rollbackRequest = (new RollbackRequest()) + ->setSession($session->name()) + ->setTransactionId($transactionId); + + $this->spannerClient->rollback($rollbackRequest, $callOptions + [ + 'resource-prefix' => $this->getDatabaseNameFromSession($session), + 'route-to-leader' => $this->routeToLeader + ]); } /** @@ -192,19 +263,36 @@ public function rollback(Session $session, $transactionId, array $options = []) * } * @return Result */ - public function execute(Session $session, $sql, array $options = []) + public function execute(Session $session, string $sql, array $options = []): Result { - $options += [ - 'parameters' => [], - 'types' => [], - 'transactionContext' => null - ]; + $options += $this->mapper->formatParamsForExecuteSql( + $options['parameters'] ?? [], + $options['types'] ?? [] + ); - $parameters = $this->pluck('parameters', $options); - $types = $this->pluck('types', $options); - $options += $this->mapper->formatParamsForExecuteSql($parameters, $types); + $options = $this->formatSqlParams($options); + $options['transaction'] = $this->createTransactionSelector($options); + $options['queryOptions'] = $this->createQueryOptions($options); + + /** + * @var ExecuteSqlRequest $executeSqlRequest + * @var array $callOptions + * @var array $miscOptions + * @var array $rtl + */ + [$executeSqlRequest, $callOptions, $miscOptions, $rtl] = $this->validateOptions( + $options, + new ExecuteSqlRequest(), + CallOptions::class, + ['parameters', 'types', 'transactionContext'], + ['route-to-leader'] + ); + $executeSqlRequest->setSql($sql); + $executeSqlRequest->setSession($session->name()); - $context = $this->pluck('transactionContext', $options); + // Spanner allows "route-to-leader" as a call option (See SpannerMiddleware) + // @TODO potentially move to a `Spanner\CallOptions` + $callOptions += $rtl; // Initially with begin, transactionId will be null. // Once transaction is generated, even in the case of stream failure, @@ -212,23 +300,20 @@ public function execute(Session $session, $sql, array $options = []) $call = function ($resumeToken = null, $transaction = null) use ( $session, $sql, - $options + $executeSqlRequest, + $callOptions ) { if ($transaction && !empty($transaction->id())) { - $options['transaction'] = ['id' => $transaction->id()]; + $executeSqlRequest->setTransaction(new TransactionSelector(['id' => $transaction->id()])); } if ($resumeToken) { - $options['resumeToken'] = $resumeToken; + $executeSqlRequest->setResumeToken($resumeToken); } - return $this->connection->executeStreamingSql([ - 'sql' => $sql, - 'session' => $session->name(), - 'database' => $this->getDatabaseNameFromSession($session) - ] + $options); + $databaseName = $this->getDatabaseNameFromSession($session); + return $this->executeStreamingSql($databaseName, $executeSqlRequest, $callOptions); }; - - return new Result($this, $session, $call, $context, $this->mapper); + return new Result($this, $session, $call, $miscOptions['transactionContext'] ?? null, $this->mapper); } /** @@ -255,13 +340,17 @@ public function execute(Session $session, $sql, array $options = []) public function executeUpdate( Session $session, Transaction $transaction, - $sql, + string $sql, array $options = [] - ) { + ): int { if (!isset($options['transaction']['begin'])) { $options['transaction'] = ['id' => $transaction->id()]; } + $statsItem = $options['statsItem'] ?? 'rowCountExact'; + unset($options['statsItem']); + $res = $this->execute($session, $sql, $options); + if (empty($transaction->id()) && $res->transaction()) { $transaction->setId($res->transaction()->id()); } @@ -276,8 +365,6 @@ public function executeUpdate( ); } - $statsItem = $options['statsItem'] ?? 'rowCountExact'; - return $stats[$statsItem]; } @@ -327,29 +414,28 @@ public function executeUpdateBatch( Transaction $transaction, array $statements, array $options = [] - ) { - $stmts = []; - foreach ($statements as $statement) { - if (!isset($statement['sql'])) { - throw new InvalidArgumentException('Each statement must contain a SQL key.'); - } - - $parameters = $this->pluck('parameters', $statement, false) ?: []; - $types = $this->pluck('types', $statement, false) ?: []; - $stmts[] = [ - 'sql' => $statement['sql'] - ] + $this->mapper->formatParamsForExecuteSql($parameters, $types); - } - - if (!isset($options['transaction']['begin'])) { - $options['transaction'] = ['id' => $transaction->id()]; - } - - $res = $this->connection->executeBatchDml([ + ): BatchDmlResult { + $options += [ 'session' => $session->name(), - 'database' => $this->getDatabaseNameFromSession($session), - 'statements' => $stmts - ] + $options); + 'statements' => $this->formatStatements($statements), + ]; + $options['transaction'] = $this->createTransactionSelector($options, $transaction->id()); + + /** + * @var ExecuteBatchDmlRequest $dmlRequest + * @var array $callOptions + */ + [$dmlRequest, $callOptions] = $this->validateOptions( + $options, + new ExecuteBatchDmlRequest(), + CallOptions::class + ); + + $response = $this->spannerClient->executeBatchDml($dmlRequest, $callOptions + [ + 'resource-prefix' => $this->getDatabaseNameFromSession($session), + 'route-to-leader' => $this->routeToLeader + ]); + $res = $this->handleResponse($response); if (empty($transaction->id())) { // Get the transaction from array of ResultSets. @@ -401,37 +487,52 @@ public function read( KeySet $keySet, array $columns, array $options = [] - ) { - $options += [ - 'index' => null, - 'limit' => null, - 'offset' => null, - 'transactionContext' => null - ]; + ): Result { + $options['transaction'] = $this->createTransactionSelector($options); + $options['keySet'] = $this->flattenKeySet($keySet); + + /** + * @var ReadRequest $readRequest + * @var array $callOptions + * @var string|null $context + * @var array $rtl + */ + [$readRequest, $callOptions, $context, $rtl] = $this->validateOptions( + $options, + new ReadRequest(), + CallOptions::class, + 'transactionContext', + ['route-to-leader'] + ); - $context = $this->pluck('transactionContext', $options); + // Spanner allows "route-to-leader" as a call option {@see Middleware\SpannerMiddleware} + $callOptions += $rtl; $call = function ($resumeToken = null, $transaction = null) use ( $table, $session, $columns, - $keySet, - $options + $readRequest, + $callOptions ) { if ($transaction && !empty($transaction->id())) { - $options['transaction'] = ['id' => $transaction->id()]; + $readRequest->setTransaction(new TransactionSelector(['id' => $transaction->id()])); } if ($resumeToken) { - $options['resumeToken'] = $resumeToken; + $readRequest->setResumeToken($resumeToken); } - return $this->connection->streamingRead([ - 'table' => $table, - 'session' => $session->name(), - 'columns' => $columns, - 'keySet' => $this->flattenKeySet($keySet), - 'database' => $this->getDatabaseNameFromSession($session) - ] + $options); + $readRequest + ->setTable($table) + ->setSession($session->name()) + ->setColumns($columns); + + // return the generator + return $this->streamingRead( + $this->getDatabaseNameFromSession($session), + $readRequest, + $callOptions + ); }; return new Result($this, $session, $call, $context, $this->mapper); @@ -455,70 +556,71 @@ public function read( * `false`. * @type array $begin The begin transaction options. * [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions) - * @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance. + * @type array $requestOptions + * @type array $transactionOptions + * @type string $tag * } * @return Transaction */ - public function transaction(Session $session, array $options = []) + public function transaction(Session $session, array $options = []): Transaction { - $options += [ - 'singleUse' => false, - 'isRetry' => false, - 'requestOptions' => [] - ]; - $transactionTag = $this->pluck('tag', $options, false); - if (isset($transactionTag)) { - $options['requestOptions']['transactionTag'] = $transactionTag; + /** + * @var array $options + * @var BeginTransactionRequest $beginTransaction + * @var TransactionSelector $transactionSelector + * @var array $callOptions + */ + [$options, $beginTransaction, $transactionSelector, $callOptions] = $this->validateOptions( + $options, + ['tag', 'isRetry', 'transactionOptions', 'singleUse'], // "singleUse" is an internal flag + new BeginTransactionRequest(), + new TransactionSelector(), + CallOptions::class, + ); + $transactionTag = $options['tag'] ?? null; + $isRetry = $options['isRetry'] ?? false; + // transaction options may be passed in as a message or array + // TODO: only allow messages + $transactionOptions = null; + if (isset($options['transactionOptions'])) { + $transactionOptions = is_array($options['transactionOptions']) + ? $this->serializer->decodeMessage( + new TransactionOptions(), + $this->formatTransactionOptions($options['transactionOptions']) + ) + : $options['transactionOptions']; } + $res = []; + if (empty($options['singleUse']) && ( + !$transactionSelector->hasBegin() + || $transactionOptions?->hasPartitionedDml() + )) { + if (!$beginTransaction->hasRequestOptions()) { + $beginTransaction->setRequestOptions(new RequestOptions()); + } + if ($transactionTag) { + $beginTransaction->getRequestOptions()->setTransactionTag($transactionTag); + } + if ($transactionOptions) { + $beginTransaction->setOptions($transactionOptions); + } - if (!$options['singleUse'] && (!isset($options['begin']) || - isset($options['transactionOptions']['partitionedDml'])) - ) { - $res = $this->beginTransaction($session, $options); - } else { - $res = []; + $res = $this->beginTransaction($session, $beginTransaction, $callOptions); } - return $this->createTransaction( - $session, - $res, - [ - 'tag' => $transactionTag, - 'isRetry' => $options['isRetry'], - 'transactionOptions' => $options - ] - ); - } - - /** - * Create a Transaction instance from a response object. - * - * @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. - * @type array $begin The begin transaction options. - * [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions) - * @return Transaction - */ - public function createTransaction(Session $session, array $res = [], array $options = []) - { - $res += [ - 'id' => null - ]; - $options += [ - 'tag' => null, - 'transactionOptions' => null - ]; - - $options['isRetry'] = $options['isRetry'] ?? false; - + $options = array_filter([ + 'isRetry' => $isRetry, + 'tag' => $transactionTag, + 'begin' => $transactionSelector->getBegin(), + 'singleUse' => $options['singleUse'] ?? null, + 'requestOptions' => $beginTransaction->getRequestOptions(), + 'transactionOptions' => $transactionOptions, + ]); return new Transaction( $this, $session, - $res['id'], - $options['isRetry'], - $options['tag'], - $options['transactionOptions'], + $res['id'] ?? null, + $options, $this->mapper ); } @@ -539,58 +641,48 @@ public function createTransaction(Session $session, array $res = [], array $opti * @type string $className If set, an instance of the given class will * be instantiated. This setting is intended for internal use. * **Defaults to** `Google\Cloud\Spanner\Snapshot`. - * @type array $directedReadOptions Directed read options. - * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} - * If using the `replicaSelection::type` setting, utilize the constants available in - * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. + * @type array|TransactionOptions $transactionOptions * } - * @return mixed + * @return TransactionalReadInterface */ - public function snapshot(Session $session, array $options = []) + public function snapshot(Session $session, array $options = []): TransactionalReadInterface { - $options += [ - 'singleUse' => false, - 'className' => Snapshot::class - ]; - - if (!$options['singleUse']) { - $res = $this->beginTransaction($session, $options); - } else { - $res = []; - } - - $className = $this->pluck('className', $options); - return $this->createSnapshot( - $session, - $res + $options, - $className + /** + * @var BeginTransactionRequest $beginTransaction + * @var array $callOptions + * @var array $misc + */ + [$beginTransaction, $callOptions, $misc] = $this->validateOptions( + $options, + new BeginTransactionRequest(), + CallOptions::class, + ['singleUse', 'className', 'transactionOptions'] ); - } + if (isset($misc['transactionOptions'])) { + $transactionOptions = is_array($misc['transactionOptions']) + ? $this->serializer->decodeMessage( + new TransactionOptions(), + $this->formatTransactionOptions($misc['transactionOptions']) + ) + : $misc['transactionOptions']; + $beginTransaction->setOptions($transactionOptions); + $options['transactionOptions'] = $transactionOptions; + } - /** - * Create a Snapshot instance from a response object. - * - * @param Session $session The session the snapshot belongs to. - * @param array $res [optional] The createTransaction response. - * @param string $className [optional] The class to instantiate with a - * snapshot. **Defaults to** `Google\Cloud\Spanner\Snapshot`. - * @return mixed - */ - public function createSnapshot(Session $session, array $res = [], $className = Snapshot::class) - { - $res += [ - 'id' => null, - 'readTimestamp' => null - ]; + $res = []; + if (false === ($misc['singleUse'] ?? false)) { + $res = $this->beginTransaction($session, $beginTransaction, $callOptions); + } - if ($res['readTimestamp']) { + $snapshotClass = $misc['className'] ?? Snapshot::class; + if (isset($res['readTimestamp'])) { if (!($res['readTimestamp'] instanceof Timestamp)) { $time = $this->parseTimeString($res['readTimestamp']); $res['readTimestamp'] = new Timestamp($time[0], $time[1]); } } - return new $className($this, $session, $res); + return new $snapshotClass($this, $session, $res + $options); } /** @@ -616,15 +708,31 @@ public function createSnapshot(Session $session, array $res = [], $className = S * } * @return Session */ - public function createSession($databaseName, array $options = []) + public function createSession(string $databaseName, array $options = []): Session { - $res = $this->connection->createSession([ + /** + * @var array $options + * @var array $callOptions + */ + [$options, $callOptions] = $this->validateOptions( + $options, + ['labels', 'creator_role'], + CallOptions::class + ); + $createSession = [ 'database' => $databaseName, 'session' => [ - 'labels' => $this->pluck('labels', $options, false) ?: [], - 'creator_role' => $this->pluck('creator_role', $options, false) ?: '' - ] - ] + $options); + 'labels' => $options['labels'] ?? [], + 'creator_role' => $options['creator_role'] ?? '' + ]]; + + $request = $this->serializer->decodeMessage(new CreateSessionRequest(), $createSession); + + $response = $this->spannerClient->createSession($request, $callOptions + [ + 'resource-prefix' => $databaseName, + 'route-to-leader' => $this->routeToLeader + ]); + $res = $this->handleResponse($response); return $this->session($res['name']); } @@ -640,15 +748,17 @@ public function createSession($databaseName, array $options = []) * @param string $sessionName The session's name. * @return Session */ - public function session($sessionName) + public function session(string $sessionName): Session { - $sessionNameComponents = GapicSpannerClient::parseName($sessionName); + $sessionNameComponents = SpannerClient::parseName($sessionName); return new Session( - $this->connection, + $this->spannerClient, + $this->serializer, $sessionNameComponents['project'], $sessionNameComponents['instance'], $sessionNameComponents['database'], - $sessionNameComponents['session'] + $sessionNameComponents['session'], + ['routeToLeader' => $this->routeToLeader] ); } @@ -689,30 +799,53 @@ public function session($sessionName) * } * @return QueryPartition[] */ - public function partitionQuery(Session $session, $transactionId, $sql, array $options = []) - { - // cache this to pass to the partition instance. - $originalOptions = $options; - - $parameters = $this->pluck('parameters', $options, false) ?: []; - $types = $this->pluck('types', $options, false) ?: []; - $options += $this->mapper->formatParamsForExecuteSql($parameters, $types); + public function partitionQuery( + Session $session, + string $transactionId, + string $sql, + array $options = [] + ): array { + $options += $this->formatPartitionQueryOptions([ + 'parameters' => $options['parameters'] ?? null, + 'types' => $options['types'] ?? null, + ]); + $options['transaction'] = $this->createTransactionSelector($options, $transactionId); + + // Split all the options into their respective categories + /** + * @var array $_paramsAndTypes + * @var PartitionOptions $partitionOptions + * @var PartitionQueryRequest $partitionQuery + * @var ExecuteSqlRequest $_executeSql + * @var array $callOptions + */ + [$_paramsAndTypes, $partitionOptions, $partitionQuery, $_executeSql, $callOptions] = $this->validateOptions( + $options, + ['parameters', 'types'], // handled above, but define them here as well (so they're validated) + new PartitionOptions(), + new PartitionQueryRequest(), + new ExecuteSqlRequest(), // these options are unused in this method, but are passed to QueryPartition + CallOptions::class + ); - $options = $this->partitionOptions($options); + $partitionQuery + ->setSession($session->name()) + ->setSql($sql) + ->setPartitionOptions($partitionOptions); - $res = $this->connection->partitionQuery([ - 'session' => $session->name(), - 'database' => $this->getDatabaseNameFromSession($session), - 'transactionId' => $transactionId, - 'sql' => $sql - ] + $options); + $response = $this->spannerClient->partitionQuery($partitionQuery, $callOptions + [ + 'resource-prefix' => $this->getDatabaseNameFromSession($session), + 'route-to-leader' => $this->routeToLeader + ]); + $res = $this->handleResponse($response); $partitions = []; + $queryPartitionOptions = $this->pluckArray(['parameters', 'types', 'maxPartitions', 'partitionSizeBytes'], $options); foreach ($res['partitions'] as $partition) { $partitions[] = new QueryPartition( $partition['partitionToken'], $sql, - $originalOptions + $queryPartitionOptions ); } @@ -745,76 +878,85 @@ public function partitionQuery(Session $session, $transactionId, $sql, array $op */ public function partitionRead( Session $session, - $transactionId, - $table, + string $transactionId, + string $table, KeySet $keySet, array $columns, array $options = [] - ) { - // cache this to pass to the partition instance. - $originalOptions = $options; + ): array { + $options['transaction'] = $this->createTransactionSelector($options, $transactionId); + $options['columns'] = $columns; + $options['keySet'] = $this->flattenKeySet($keySet); + + // Split all the options into their respective categories. + // $readRequest is unused, but the options are valid because they're passed in to the + // constructor of ReadPartition. + /** + * @var PartitionOptions $partitionOptions + * @var PartitionReadRequest $partitionRead + * @var ReadRequest $_readRequest + * @var array $callOptions + */ + [$partitionOptions, $partitionRead, $_readRequest, $callOptions] = $this->validateOptions( + $options, + new PartitionOptions(), + new PartitionReadRequest(), + new ReadRequest(), // these options are unused in this method, but are passed to ReadPartition + CallOptions::class + ); - $options = $this->partitionOptions($options); + $partitionRead->setSession($session->name()); + $partitionRead->setTable($table); + $partitionRead->setPartitionOptions($partitionOptions); - $res = $this->connection->partitionRead([ - 'session' => $session->name(), - 'database' => $this->getDatabaseNameFromSession($session), - 'transactionId' => $transactionId, - 'table' => $table, - 'columns' => $columns, - 'keySet' => $this->flattenKeySet($keySet) - ] + $options); + $response = $this->spannerClient->partitionRead($partitionRead, $callOptions + [ + 'resource-prefix' => $this->getDatabaseNameFromSession($session), + 'route-to-leader' => $this->routeToLeader + ]); + $res = $this->handleResponse($response); $partitions = []; + $readPartitionOptions = $this->pluckArray(['index', 'maxPartitions', 'partitionSizeBytes'], $options); foreach ($res['partitions'] as $partition) { $partitions[] = new ReadPartition( $partition['partitionToken'], $table, $keySet, $columns, - $originalOptions + $readPartitionOptions ); } return $partitions; } - /** - * Normalize options for partition configuration. - * - * @param array $options - * @return array - */ - private function partitionOptions(array $options) - { - $options['partitionOptions'] = array_filter([ - 'partitionSizeBytes' => $this->pluck('partitionSizeBytes', $options, false), - 'maxPartitions' => $this->pluck('maxPartitions', $options, false) - ]); - - return $options; - } - /** * 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. + * @param BeginTransactionRequest $beginTransaction + * @param array $callOptions * * @return array */ - private function beginTransaction(Session $session, array $options = []) + private function beginTransaction(Session $session, BeginTransactionRequest $beginTransaction, array $callOptions): array { - $options += [ - 'transactionOptions' => [] - ]; + $routeToLeader = $this->routeToLeader && ( + $beginTransaction->getOptions()?->hasReadWrite() + || $beginTransaction->getOptions()?->hasPartitionedDml() + ); - return $this->connection->beginTransaction($options + [ - 'session' => $session->name(), - 'database' => $this->getDatabaseNameFromSession($session) + if (!$beginTransaction->getSession()) { + $beginTransaction->setSession($session->name()); + } + + $response = $this->spannerClient->beginTransaction($beginTransaction, $callOptions + [ + 'resource-prefix' => $this->getDatabaseNameFromSession($session), + 'route-to-leader' => $routeToLeader, ]); + return $this->handleResponse($response); } /** @@ -823,7 +965,7 @@ private function beginTransaction(Session $session, array $options = []) * @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) + private function flattenKeySet(KeySet $keySet): array { $keys = $keySet->keySetObject(); @@ -844,11 +986,212 @@ private function flattenKeySet(KeySet $keySet) return $this->arrayFilterRemoveNull($keys); } - private function getDatabaseNameFromSession(Session $session) + private function getDatabaseNameFromSession(Session $session): string { return $session->info()['databaseName']; } + /** + * Serialize the mutations. + * + * @param array $mutations + * @return array + */ + private function serializeMutations(array $mutations): array + { + $serializedMutations = []; + if (is_array($mutations)) { + foreach ($mutations as $mutation) { + $type = array_keys($mutation)[0]; + $data = $mutation[$type]; + + switch ($type) { + case Operation::OP_DELETE: + // nothing to do + break; + default: + $modifiedData = array_map([$this, 'formatValueForApi'], $data['values']); + $data['values'] = [['values' => $modifiedData]]; + + break; + } + + $serializedMutations[] = [$type => $data]; + } + } + + return $serializedMutations; + } + + /** + * Format statements. + * + * @param array $statements + * @return array + */ + private function formatStatements(array $statements): array + { + $result = []; + foreach ($statements as $statement) { + if (!isset($statement['sql'])) { + throw new InvalidArgumentException('Each statement must contain a SQL key.'); + } + + $parameters = $statement['parameters'] ?? []; + $types = $statement['types'] ?? []; + $mappedStatement = [ + 'sql' => $statement['sql'] + ] + $this->mapper->formatParamsForExecuteSql($parameters, $types); + + $result[] = $this->formatSqlParams($mappedStatement); + } + return $result; + } + + /** + * @param array $args + * @return array + */ + private function formatSqlParams(array $args): array + { + $params = $this->pluck('params', $args); + if ($params) { + $modifiedParams = array_map([$this, 'formatValueForApi'], $params); + $args['params'] = ['fields' => $modifiedParams]; + } + + return $args; + } + + /** + * @param array $args + * @param string|null $transactionId + * + * @return array + */ + private function createTransactionSelector( + array $args, + string|null $transactionId = null + ): array { + if (isset($args['transaction'])) { + $transactionSelector = $args['transaction']; + + if (isset($transactionSelector['singleUse'])) { + $transactionSelector['singleUse'] = + $this->formatTransactionOptions($transactionSelector['singleUse']); + } + + if (isset($transactionSelector['begin'])) { + $transactionSelector['begin'] = + $this->formatTransactionOptions($transactionSelector['begin']); + } + return $transactionSelector; + } + + if ($transactionId) { + return ['id' => $transactionId]; + } + + return []; + } + + /** + * @param array $args + * + * @return array + */ + private function createQueryOptions(array $args): array + { + $queryOptions = $this->pluck('queryOptions', $args, false) ?: []; + // Query options precedence is query-level, then environment-level, then client-level. + $envQueryOptimizerVersion = getenv('SPANNER_OPTIMIZER_VERSION'); + $envQueryOptimizerStatisticsPackage = getenv('SPANNER_OPTIMIZER_STATISTICS_PACKAGE'); + if (!empty($envQueryOptimizerVersion)) { + $queryOptions += ['optimizerVersion' => $envQueryOptimizerVersion]; + } + if (!empty($envQueryOptimizerStatisticsPackage)) { + $queryOptions += ['optimizerStatisticsPackage' => $envQueryOptimizerStatisticsPackage]; + } + $queryOptions += $this->defaultQueryOptions ?: []; + + return $queryOptions; + } + + /** + * @param array $transactionOptions + * @return array + */ + private function formatTransactionOptions(array $transactionOptions): array + { + // sometimes readOnly is a PBReadOnly message instance + if (isset($transactionOptions['readOnly']) && is_array($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; + } + + /** + * @param string $databaseName + * @param ExecuteSqlRequest $executeSqlRequest + * @param array $callOptions + * @return Generator + */ + private function executeStreamingSql(string $databaseName, ExecuteSqlRequest $executeSqlRequest, array $callOptions): Generator + { + if (!$this->routeToLeader) { + unset($callOptions['route-to-leader']); + } + + $response = $this->spannerClient->executeStreamingSql($executeSqlRequest, $callOptions + [ + 'resource-prefix' => $databaseName, + ]); + + return $this->handleResponse($response); + } + + /** + * @param string $databaseName + * @param ReadRequest $readRequest + * @param array $callOptions + * @return Generator + */ + private function streamingRead(string $databaseName, ReadRequest $readRequest, array $callOptions): Generator + { + if (!$this->routeToLeader) { + unset($callOptions['route-to-leader']); + } + + $response = $this->spannerClient->streamingRead($readRequest, $callOptions + [ + 'resource-prefix' => $databaseName, + ]); + + return $this->handleResponse($response); + } + + /** + * @param array $args + * + * @return array{params: array, paramTypes: array} + */ + private function formatPartitionQueryOptions(array $args): array + { + $parameters = $args['parameters'] ?? []; + $types = $args['types'] ?? []; + + $paramsAndParamTypes = $this->mapper->formatParamsForExecuteSql($parameters, $types); + return $this->formatSqlParams($paramsAndParamTypes); + } + /** * Represent the class in a more readable and digestable fashion. * @@ -858,7 +1201,7 @@ private function getDatabaseNameFromSession(Session $session) public function __debugInfo() { return [ - 'connection' => get_class($this->connection), + 'spannerClient' => get_class($this->spannerClient), ]; } } diff --git a/Spanner/src/PgJsonb.php b/Spanner/src/PgJsonb.php index 5788e7dbe8e4..95c8deaaf14b 100644 --- a/Spanner/src/PgJsonb.php +++ b/Spanner/src/PgJsonb.php @@ -30,7 +30,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $pgJsonb = $spanner->pgJsonb('{}'); * ``` */ @@ -38,15 +38,9 @@ class PgJsonb implements ValueInterface, TypeAnnotationInterface { use JsonTrait; - /** - * @var string|null - */ - private $value; + private string|null $value; - /** - * @param string|array|JsonSerializable|null $value The value to be used as the JSONB string. - */ - public function __construct($value) + public function __construct(string|array|JsonSerializable|null $value) { // null shouldn't be casted to an empty string if (!is_null($value)) { @@ -64,7 +58,7 @@ public function __construct($value) * * @return string|null */ - public function get() + public function get(): string|null { return $this->value; } @@ -75,7 +69,7 @@ public function get() * @access private * @return int */ - public function type() + public function type(): int { return ValueMapper::TYPE_JSON; } @@ -87,7 +81,7 @@ public function type() * @access private * @return int */ - public function typeAnnotation() + public function typeAnnotation(): int { return TypeAnnotationCode::PG_JSONB; } @@ -97,7 +91,7 @@ public function typeAnnotation() * * @return string */ - public function formatAsString() + public function formatAsString(): string { return (string) $this->value; } diff --git a/Spanner/src/PgNumeric.php b/Spanner/src/PgNumeric.php index 8128d3208502..e18e86e3e22b 100644 --- a/Spanner/src/PgNumeric.php +++ b/Spanner/src/PgNumeric.php @@ -31,22 +31,19 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $pgNumeric = $spanner->pgNumeric('99999999999999999999999999999999999999.000000999999999'); * ``` */ class PgNumeric implements ValueInterface, TypeAnnotationInterface { - /** - * @var string|null - */ - private $value; + private string|null $value; /** * @param string|int|float|null $value The PG_NUMERIC value. */ - public function __construct($value) + public function __construct(string|int|float|null $value) { // null shouldn't be casted to an empty string $value = is_null($value) ? $value : (string) $value; @@ -58,7 +55,7 @@ public function __construct($value) * * @return string|null */ - public function get() + public function get(): string|null { return $this->value; } @@ -69,7 +66,7 @@ public function get() * @access private * @return int */ - public function type() + public function type(): int { return ValueMapper::TYPE_NUMERIC; } @@ -81,7 +78,7 @@ public function type() * @access private * @return int */ - public function typeAnnotation() + public function typeAnnotation(): int { return TypeAnnotationCode::PG_NUMERIC; } @@ -91,7 +88,7 @@ public function typeAnnotation() * * @return string */ - public function formatAsString() + public function formatAsString(): string { return (string) $this->value; } diff --git a/Spanner/src/PgOid.php b/Spanner/src/PgOid.php index 8a3d0b9a1ced..ef2878fe4428 100644 --- a/Spanner/src/PgOid.php +++ b/Spanner/src/PgOid.php @@ -28,21 +28,18 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $pgOid = $spanner->pgOid('123'); * ``` */ class PgOid implements ValueInterface, TypeAnnotationInterface { - /** - * @var string|null - */ - private ?string $value; + private string|null $value; /** * @param string|null $value The OID value. */ - public function __construct(?string $value) + public function __construct(string|null $value) { $this->value = $value; } @@ -52,7 +49,7 @@ public function __construct(?string $value) * * @return string|null */ - public function get(): ?string + public function get(): string|null { return $this->value; } @@ -85,7 +82,7 @@ public function typeAnnotation(): int * * @return string */ - public function formatAsString(): ?string + public function formatAsString(): string { return (string) $this->value; } diff --git a/Spanner/src/RequestHeaderTrait.php b/Spanner/src/RequestHeaderTrait.php deleted file mode 100644 index 456402c38e1c..000000000000 --- a/Spanner/src/RequestHeaderTrait.php +++ /dev/null @@ -1,84 +0,0 @@ -resumeOperation()`. + * @return ItemIterator + */ + private function buildLongRunningIterator( + callable $call, + Message $request, + array $callOptions, + callable $resultMapper + ): ItemIterator { + $resultLimit = $this->pluck('resultLimit', $callOptions, false) ?: 0; + return new ItemIterator( + new PageIterator( + $resultMapper, + function (array $args) use ($call) { + if ($pageToken = $this->pluck('pageToken', $args, false) ?: null) { + $args['request']->setPageToken($pageToken); + } + try { + $page = $call($args['request'], $args['callOptions'])->getPage(); + } catch (ApiException $e) { + throw $this->convertToGoogleException($e); + } + return [ + 'operations' => iterator_to_array($page->getResponseObject()->getOperations()), + 'nextResultToken' => $page->getNextPageToken(), + ]; + }, + [ + 'request' => $request, + 'callOptions' => $callOptions + ], + [ + 'itemsKey' => 'operations', + 'resultLimit' => $resultLimit + ] + ) + ); + } + + private function buildListItemsIterator( + callable $call, + Message $request, + array $callOptions, + callable $resultMapper, + string $itemsKey, + ?int $resultLimit = null + ) { + return new ItemIterator( + new PageIterator( + $resultMapper, + function ($args) use ($call) { + if ($pageToken = $this->pluck('pageToken', $args, false) ?: null) { + $args['request']->setPageToken($pageToken); + } + $response = $call($args['request'], $args['callOptions']); + return $this->handleResponse($response); + }, + [ + 'request' => $request, + 'callOptions' => $callOptions + ], + [ + 'itemsKey' => $itemsKey, + 'resultLimit' => $resultLimit + ] + ) + ); + } + + private function operationFromOperationResponse( + OperationResponse $operation + ): LongRunningOperation { + if (!method_exists($this, 'resumeOperation')) { + throw new \BadMethodCallException('This class must implement resumeOperation to call this method.'); + } + return $this->resumeOperation( + (string) $operation->getName(), + $this->handleResponse($operation->getLastProtoResponse()) ?? [] + ); + } +} diff --git a/Spanner/src/Result.php b/Spanner/src/Result.php index b335804a2331..54fff45198d7 100644 --- a/Spanner/src/Result.php +++ b/Spanner/src/Result.php @@ -17,8 +17,11 @@ namespace Google\Cloud\Spanner; +use Generator; +use Google\ApiCore\RetrySettings; use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Core\ExponentialBackoff; +use Google\Cloud\Core\TimeTrait; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\V1\ExecuteSqlRequest\QueryMode; @@ -31,7 +34,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $database = $spanner->connect('my-instance', 'my-database'); * $result = $database->execute('SELECT * FROM Posts'); @@ -41,90 +44,30 @@ */ class Result implements \IteratorAggregate { - const BUFFER_RESULT_LIMIT = 10; + use TimeTrait; + private const BUFFER_RESULT_LIMIT = 10; const RETURN_NAME_VALUE_PAIR = 'nameValuePair'; const RETURN_ASSOCIATIVE = 'associative'; const RETURN_ZERO_INDEXED = 'zeroIndexed'; - const MODE_NORMAL = QueryMode::NORMAL; const MODE_PLAN = QueryMode::PLAN; const MODE_PROFILE = QueryMode::PROFILE; - /** - * @var array - */ - private $columns = []; - - /** - * @var int - */ - private $columnCount = 0; - - /** - * @var array|null - */ - private $columnNames; - - /** - * @var ValueMapper - */ - private $mapper; - - /** - * @var array|null - */ - private $metadata; - - /** - * @var Operation - */ - private $operation; - - /** - * @var int - */ - private $retries; - - /** - * @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; - + private array $columns = []; + private int $columnCount = 0; + private array $columnNames; + private array|null $metadata; + private int $retries; + private string|null $resumeToken = null; + private TransactionalReadInterface|null $snapshot; + private array|bool|null $stats; + private Transaction|null $transaction = null; /** * @var callable */ private $call; - - /** - * @var \Generator - */ - private $generator; + private Generator $generator; /** * @param Operation $operation Runs operations against Google Cloud Spanner. @@ -132,23 +75,25 @@ class Result implements \IteratorAggregate * @param callable $call A callable, yielding a generator filled with results. * @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. + * @param RetrySettings|null $retrySettings { + * Retry configuration options. Currently, only the `maxRetries` option + * is supported. + * + * @type int $maxRetries The maximum number of retry attempts before the operation + * fails. Defaults to 3. + * } */ public function __construct( - Operation $operation, - Session $session, + private Operation $operation, + private Session $session, callable $call, - $transactionContext, - ValueMapper $mapper, - $retries = 3 + private string|null $transactionContext, + private ValueMapper $mapper, + RetrySettings|null $retrySettings = null ) { - $this->operation = $operation; $this->session = $session; $this->call = $call; - $this->transactionContext = $transactionContext; - $this->mapper = $mapper; - $this->retries = $retries; + $this->retries = isset($retrySettings) ? $retrySettings->getMaxRetries() : 3; $this->createGenerator(); } @@ -173,12 +118,12 @@ public function __construct( * 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 + * @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($format = self::RETURN_ASSOCIATIVE) + public function rows($format = self::RETURN_ASSOCIATIVE): Generator { $bufferedResults = []; $call = $this->call; @@ -265,9 +210,9 @@ public function rows($format = self::RETURN_ASSOCIATIVE) * * @return array|null */ - public function columns() + public function columns(): array|null { - return $this->columnNames; + return $this->columnNames ?? null; } /** @@ -284,7 +229,7 @@ public function columns() * @return array|null [ResultSetMetadata](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetMetadata). * @codingStandardsIgnoreEnd */ - public function metadata() + public function metadata(): array|null { return $this->metadata; } @@ -299,7 +244,7 @@ public function metadata() * * @return Session */ - public function session() + public function session(): Session { return $this->session; } @@ -333,9 +278,9 @@ public function session() * @return array|null [ResultSetStats](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetStats). * @codingStandardsIgnoreEnd */ - public function stats() + public function stats(): array|bool|null { - return $this->stats; + return $this->stats ?? null; } /** @@ -348,9 +293,9 @@ public function stats() * $snapshot = $result->snapshot(); * ``` * - * @return Snapshot|null + * @return TransactionalReadInterface|null */ - public function snapshot() + public function snapshot(): TransactionalReadInterface|null { return $this->snapshot; } @@ -367,17 +312,17 @@ public function snapshot() * * @return Transaction|null */ - public function transaction() + public function transaction(): Transaction|null { return $this->transaction; } /** * @access private - * @return \Generator + * @return Generator */ #[\ReturnTypeWillChange] - public function getIterator() + public function getIterator(): Generator { return $this->rows(); } @@ -386,7 +331,7 @@ public function getIterator() * @param array $bufferedResults * @return array */ - private function parseRowsFromBufferedResults(array $bufferedResults) + private function parseRowsFromBufferedResults(array $bufferedResults): array { $values = []; $chunkedResult = null; @@ -432,7 +377,7 @@ private function parseRowsFromBufferedResults(array $bufferedResults) * @param string $format * @throws \RuntimeException */ - private function setResultData(array $result, $format) + private function setResultData(array $result, $format): void { $this->stats = $result['stats'] ?? null; @@ -475,7 +420,7 @@ private function setResultData(array $result, $format) * @param array $set2 * @return array */ - private function mergeValues(array $set1, array $set2) + private function mergeValues(array $set1, array $set2): array { // `$set2` may be empty if an array value is chunked at the end of the // list. Handling it normally results in an additional `null` value @@ -507,7 +452,7 @@ private function mergeValues(array $set1, array $set2) * @param string $key * @return bool */ - private function isSetAndTrue($arr, $key) + private function isSetAndTrue($arr, $key): bool { return isset($arr[$key]) && $arr[$key]; } @@ -517,11 +462,12 @@ private function isSetAndTrue($arr, $key) * * @return bool Whether or not the created generator is valid. */ - private function createGenerator() + private function createGenerator(): bool { - if (!is_null($this->generator)) { + if (isset($this->generator)) { return $this->generator->valid(); } + $call = $this->call; $generator = null; @@ -549,18 +495,29 @@ private function createGenerator() * * @param array $result The streaming call response from the server. */ - private function setSnapshotOrTransaction(array $result) + private function setSnapshotOrTransaction(array $result): void { if (!empty($result['metadata']['transaction']['id'])) { + $res = $result['metadata']['transaction']; if ($this->transactionContext === SessionPoolInterface::CONTEXT_READ) { - $this->snapshot = $this->snapshot ?? $this->operation->createSnapshot( + if (isset($res['readTimestamp'])) { + if (!($res['readTimestamp'] instanceof Timestamp)) { + $time = $this->parseTimeString($res['readTimestamp']); + $res['readTimestamp'] = new Timestamp($time[0], $time[1]); + } + } + $this->snapshot = $this->snapshot ?? new Snapshot( + $this->operation, $this->session, - $result['metadata']['transaction'] + $res ); } else { - $this->transaction = $this->transaction ?? $this->operation->createTransaction( + $this->transaction = $this->transaction ?? new Transaction( + $this->operation, $this->session, - $result['metadata']['transaction'] + $res['id'], + [], + $this->mapper ); } } diff --git a/Spanner/src/Serializer.php b/Spanner/src/Serializer.php new file mode 100644 index 000000000000..641ebacdcc8b --- /dev/null +++ b/Spanner/src/Serializer.php @@ -0,0 +1,232 @@ + function ($v) { + return $this->flattenValue($v); + }, + 'google.protobuf.ListValue' => function ($v) { + return $this->flattenListValue($v); + }, + 'google.protobuf.Struct' => function ($v) { + return $this->flattenStruct($v); + }, + 'google.protobuf.Timestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + } + ]; + $decodeFieldTransformers = []; + $decodeMessageTypeTransformers = [ + 'google.spanner.v1.KeySet' => function ($keySet) { + $keys = $this->pluck('keys', $keySet, false); + if ($keys) { + $keySet['keys'] = array_map( + fn ($key) => $this->formatListForApi((array) $key), + $keys + ); + } + + if (isset($keySet['ranges'])) { + $keySet['ranges'] = array_map(function ($rangeItem) { + return array_map([$this, 'formatListForApi'], $rangeItem); + }, $keySet['ranges']); + + if (empty($keySet['ranges'])) { + unset($keySet['ranges']); + } + } + + return $keySet; + }, + 'google.protobuf.Struct' => function ($v) { + if (!isset($v['fields'])) { + return ['fields' => $v]; + } + return $v; + }, + 'google.protobuf.Value' => function ($v) { + if (!is_array($v) || ( + !isset($v['nullValue']) && + !isset($v['null_value']) && + !isset($v['numberValue']) && + !isset($v['number_value']) && + !isset($v['stringValue']) && + !isset($v['string_value']) && + !isset($v['boolValue']) && + !isset($v['bool_value']) && + !isset($v['structValue']) && + !isset($v['struct_value']) && + !isset($v['listValue']) && + !isset($v['list_value']) + )) { + return $this->formatValueForApi($v); + } + return $v; + }, + ]; + $customEncoders = [ + // A custom encoder that short-circuits the encodeMessage in Serializer class, + // but only if the argument is of the type PartialResultSet. + PartialResultSet::class => function ($msg) { + $data = json_decode($msg->serializeToJsonString(), true); + + // We only override metadata fields, if it actually exists in the response. + // This is specially important for large data sets which is received in chunks. + // Metadata is only received in the first 'chunk' and we don't want to set empty metadata fields + // when metadata was not returned from the server. + if (isset($data['metadata'])) { + // The transaction id is serialized as a base64 encoded string in $data. So, we + // add a step to get the transaction id using a getter instead of the serialized value. + // The null-safe operator is used to handle edge cases where the relevant fields are not present. + $data['metadata']['transaction']['id'] = (string) $msg?->getMetadata()?->getTransaction()?->getId(); + + // Helps convert metadata enum values from string types to their respective code/annotation + // pairs. Ex: INT64 is converted to {code: 2, typeAnnotation: 0}. + $fields = $msg->getMetadata()?->getRowType()?->getFields(); + $data['metadata']['rowType']['fields'] = $this->getFieldDataFromRepeatedFields($fields); + } + + // These fields in stats should be an int + if (isset($data['stats']['rowCountLowerBound'])) { + $data['stats']['rowCountLowerBound'] = (int) $data['stats']['rowCountLowerBound']; + } + if (isset($data['stats']['rowCountExact'])) { + $data['stats']['rowCountExact'] = (int) $data['stats']['rowCountExact']; + } + + return $data; + } + ]; + + parent::__construct( + $fieldTransformers, + $messageTypeTransformers, + $decodeFieldTransformers, + $decodeMessageTypeTransformers, + $customEncoders, + ); + + $this->serializer = $this; + } + + /** + * Utility method to return "fields data" in the format: + * [ + * "name" => "" + * "type" => [] + * ]. + * + * The type is converted from a string like INT64 to ["code" => 2, "typeAnnotation" => 0] + * conforming with the Google\Cloud\Spanner\V1\TypeCode class. + * + * @param RepeatedField|DeprecatedRepeatedField|null $fields The array contain list of fields. + * + * @return array The formatted fields data. + */ + private function getFieldDataFromRepeatedFields(RepeatedField|DeprecatedRepeatedField|null $fields): array + { + if (is_null($fields)) { + return []; + } + + $fieldsData = []; + foreach ($fields as $key => $field) { + $type = $field->getType(); + $typeData = $this->getTypeData($type); + + $fieldsData[$key] = [ + 'name' => $field->getName(), + 'type' => $typeData + ]; + } + + return $fieldsData; + } + + /** + * Utiltiy method to take in a Google\Cloud\Spanner\V1\Type value and return + * the data as an array. The method takes care of array and struct elements. + * + * @param Type $type The "type" object + * + * @return array The formatted data. + */ + private function getTypeData(Type $type): array + { + $data = [ + 'code' => $type->getCode(), + 'typeAnnotation' => $type->getTypeAnnotation(), + 'protoTypeFqn' => $type->getProtoTypeFqn() + ]; + + // If this is a struct field, then recursisevly call getTypeData + if ($type->hasStructType()) { + $nestedType = $type->getStructType(); + $fields = $nestedType->getFields(); + $data['structType'] = [ + 'fields' => $this->getFieldDataFromRepeatedFields($fields) + ]; + } + // If this is an array field, then recursisevly call getTypeData + if ($type->hasArrayElementType()) { + $nestedType = $type->getArrayElementType(); + $data['arrayElementType'] = $this->getTypeData($nestedType); + } + + return $data; + } +} diff --git a/Spanner/src/Session/CacheSessionPool.php b/Spanner/src/Session/CacheSessionPool.php index aeec330635e8..e618bb357f78 100644 --- a/Spanner/src/Session/CacheSessionPool.php +++ b/Spanner/src/Session/CacheSessionPool.php @@ -83,7 +83,7 @@ * use Google\Cloud\Spanner\Session\CacheSessionPool; * use Symfony\Component\Cache\Adapter\FilesystemAdapter; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $cache = new FilesystemAdapter(); * $sessionPool = new CacheSessionPool($cache); * @@ -111,7 +111,7 @@ * use Google\Cloud\Spanner\Session\CacheSessionPool; * use Symfony\Component\Cache\Adapter\FilesystemAdapter; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $cache = new FilesystemAdapter(); * $sessionPool = new CacheSessionPool($cache, [ * 'databaseRole' => 'Reader' @@ -232,7 +232,7 @@ 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 () { + [$session, $toCreate] = $this->config['lock']->synchronize(function () { $toCreate = []; $session = null; $shouldSave = false; @@ -403,7 +403,7 @@ public function keepAlive(Session $session) * @param int $percent The percentage to downsize the pool by. Must be * between 1 and 100. * @return int The number of sessions removed from the pool. - * @throws \InvaldArgumentException + * @throws \InvalidArgumentException * @throws \RuntimeException */ public function downsize($percent) @@ -690,8 +690,7 @@ private function createSessions($count) // @see https://github.com/googleapis/google-cloud-php/pull/2342#discussion_r327925546 while ($count > $created) { try { - $res = $this->database->connection()->batchCreateSessions([ - 'database' => $this->database->name(), + $res = $this->database->batchCreateSessions([ 'sessionTemplate' => [ 'labels' => isset($this->config['labels']) ? $this->config['labels'] : [], 'creator_role' => isset($this->config['databaseRole']) ? $this->config['databaseRole'] : '' @@ -765,6 +764,8 @@ private function handleSession(array $session) unset($data['inUse'][$session['name']]); $this->save($item->set($data)); }); + + return null; } /** @@ -873,11 +874,9 @@ private function deleteSessions(array $sessions, $waitForPromises = false) { $this->deleteCalls = []; foreach ($sessions as $session) { - $this->deleteCalls[] = $this->database->connection() - ->deleteSessionAsync([ - 'name' => $session['name'], - 'database' => $this->database->name() - ]); + $this->deleteCalls[] = $this->database->deleteSessionAsync([ + 'name' => $session['name'] + ]); } if ($waitForPromises && !empty($this->deleteCalls)) { diff --git a/Spanner/src/Session/Session.php b/Spanner/src/Session/Session.php index be909d6c7892..033867a115d6 100644 --- a/Spanner/src/Session/Session.php +++ b/Spanner/src/Session/Session.php @@ -17,71 +17,60 @@ namespace Google\Cloud\Spanner\Session; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\Exception\NotFoundException; -use Google\Cloud\Spanner\Connection\ConnectionInterface; -use Google\Cloud\Spanner\V1\SpannerClient; +use Google\Cloud\Spanner\Database; +use Google\Cloud\Spanner\Serializer; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\DeleteSessionRequest; +use Google\Cloud\Spanner\V1\GetSessionRequest; /** * Represents and manages a single Cloud Spanner session. */ class Session { - /** - * @var ConnectionInterface - * @internal - */ - private $connection; + use ApiHelperTrait; /** - * @var string - */ - private $projectId; - - /** - * @var string - */ - private $instance; - - /** - * @var string + * @var int|null */ - private $database; + private int|null $expiration = null; /** - * @var string + * @var bool */ - private $databaseName; + private bool $routeToLeader; /** * @var string */ - private $name; + private string $databaseName; /** - * @var int|null - */ - private $expiration; - - /** - * @param ConnectionInterface $connection A connection to Cloud Spanner. - * This object is created by SpannerClient, - * and should not be instantiated outside of this client. + * @internal Session is constructed by the {@see Database} class. + * + * @param Serializer $serializer The serializer instance to encode/decode messages. * @param string $projectId The project ID. * @param string $instance The instance name. * @param string $database The database name. * @param string $name The session name. + * @param array $config [optional] { + * Configuration options. + * + * @type bool $routeToLeader Enable/disable Leader Aware Routing. + * **Defaults to** `true` (enabled). + * } */ public function __construct( - ConnectionInterface $connection, - $projectId, - $instance, - $database, - $name + private SpannerClient $spannerClient, + private Serializer $serializer, + private string $projectId, + private string $instance, + private string $database, + private string $name, + array $config = [] ) { - $this->connection = $connection; - $this->projectId = $projectId; - $this->instance = $instance; - $this->database = $database; $this->databaseName = SpannerClient::databaseName( $projectId, $instance, @@ -93,6 +82,7 @@ public function __construct( $database, $name ); + $this->routeToLeader = $this->pluck('routeToLeader', $config, false) ?? true; } /** @@ -101,7 +91,7 @@ public function __construct( * @return array An array containing the `projectId`, `instance`, `database`, 'databaseName' and session `name` * keys. */ - public function info() + public function info(): array { return [ 'projectId' => $this->projectId, @@ -118,18 +108,24 @@ public function info() * @param array $options [optional] Configuration options. * @return bool */ - public function exists(array $options = []) + public function exists(array $options = []): bool { + [$data, $callOptions] = $this->splitOptionalArgs($options); + $data += [ + 'name' => $this->name() + ]; + try { - $this->connection->getSession($options + [ - 'name' => $this->name(), - 'database' => $this->databaseName - ]); + $request = $this->serializer->decodeMessage(new GetSessionRequest(), $data); - return true; + $this->spannerClient->getSession($request, $callOptions + [ + 'resource-prefix' => $this->databaseName, + 'route-to-leader' => $this->routeToLeader, + ]); } catch (NotFoundException $e) { return false; } + return true; } /** @@ -138,11 +134,17 @@ public function exists(array $options = []) * @param array $options [optional] Configuration options. * @return void */ - public function delete(array $options = []) + public function delete(array $options = []): void { - $this->connection->deleteSession($options + [ - 'name' => $this->name(), - 'database' => $this->databaseName + [$data, $callOptions] = $this->splitOptionalArgs($options); + $data = [ + 'name' => $this->name() + ]; + + $request = $this->serializer->decodeMessage(new DeleteSessionRequest(), $data); + + $this->spannerClient->deleteSession($request, $callOptions + [ + 'resource-prefix' => $this->databaseName, ]); } @@ -151,7 +153,7 @@ public function delete(array $options = []) * * @return string */ - public function name() + public function name(): string { return $this->name; } @@ -164,7 +166,7 @@ public function name() * minutes. * @return void */ - public function setExpiration($expiration = null) + public function setExpiration($expiration = null): void { $this->expiration = $expiration ?: time() + SessionPoolInterface::SESSION_EXPIRATION_SECONDS; } @@ -174,7 +176,7 @@ public function setExpiration($expiration = null) * * @return int|null */ - public function expiration() + public function expiration(): int|null { return $this->expiration; } @@ -188,7 +190,7 @@ public function expiration() public function __debugInfo() { return [ - 'connection' => get_class($this->connection), + 'spannerClient' => isset($this->spannerClient) ? get_class($this->spannerClient) : '', 'projectId' => $this->projectId, 'instance' => $this->instance, 'database' => $this->database, diff --git a/Spanner/src/Snapshot.php b/Spanner/src/Snapshot.php index 759bc3715f8d..68c192fad505 100644 --- a/Spanner/src/Snapshot.php +++ b/Spanner/src/Snapshot.php @@ -29,7 +29,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $database = $spanner->connect('my-instance', 'my-database'); * $transaction = $database->snapshot(); @@ -52,6 +52,7 @@ class Snapshot implements TransactionalReadInterface * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} * If using the `replicaSelection::type` setting, utilize the constants available in * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. + * @type array $transactonOptions * } * @throws \InvalidArgumentException if a tag is specified as this is a read-only transaction. */ diff --git a/Spanner/src/SnapshotTrait.php b/Spanner/src/SnapshotTrait.php index 760c978fa79d..ee07b8dbbf82 100644 --- a/Spanner/src/SnapshotTrait.php +++ b/Spanner/src/SnapshotTrait.php @@ -17,14 +17,17 @@ namespace Google\Cloud\Spanner; +use Google\ApiCore\ArrayTrait; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\V1\TransactionOptions; /** * Common methods for Read-Only transactions (i.e. Snapshots) */ trait SnapshotTrait { + use ArrayTrait; use TransactionalReadTrait; /** @@ -45,13 +48,14 @@ trait SnapshotTrait * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} * If using the `replicaSelection::type` setting, utilize the constants available in * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. + * @type array $transactionOptions The Transaction Options * } */ private function initialize( Operation $operation, Session $session, array $options = [] - ) { + ): void { $this->operation = $operation; $this->session = $session; @@ -64,15 +68,19 @@ private function initialize( throw new \InvalidArgumentException('$options.readTimestamp must be an instance of Timestamp.'); } - $this->transactionId = $options['id'] ?: null; - $this->readTimestamp = $options['readTimestamp']; - $this->type = $options['id'] + $this->transactionId = $this->pluck('id', $options) ?: null; + $this->readTimestamp = $this->pluck('readTimestamp', $options) ?: null; + $this->type = $this->transactionId ? self::TYPE_PRE_ALLOCATED : self::TYPE_SINGLE_USE; $this->context = SessionPoolInterface::CONTEXT_READ; $this->directedReadOptions = $options['directedReadOptions'] ?? []; - $this->options = $options; + $this->transactionSelector = array_intersect_key( + (array) $options, + array_flip(['singleUse', 'begin']) + ); + $this->transactionOptions = $options['transactionOptions'] ?? new TransactionOptions(); } /** @@ -88,7 +96,7 @@ private function initialize( * * @return Timestamp */ - public function readTimestamp() + public function readTimestamp(): Timestamp { return $this->readTimestamp; } diff --git a/Spanner/src/SpannerClient.php b/Spanner/src/SpannerClient.php index dc952ee89581..93ae43e80232 100644 --- a/Spanner/src/SpannerClient.php +++ b/Spanner/src/SpannerClient.php @@ -17,28 +17,36 @@ namespace Google\Cloud\Spanner; +use Google\ApiCore\ClientOptionsTrait; +use Google\ApiCore\CredentialsWrapper; +use Google\ApiCore\Middleware\MiddlewareInterface; +use Google\ApiCore\Options\CallOptions; use Google\ApiCore\ValidationException; use Google\Auth\FetchAuthTokenInterface; -use Google\Cloud\Core\ArrayTrait; -use Google\Cloud\Core\ClientTrait; +use Google\Cloud\Core\DetectProjectIdTrait; +use Google\Cloud\Core\EmulatorTrait; use Google\Cloud\Core\Exception\GoogleException; use Google\Cloud\Core\Int64; use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\Iterator\PageIterator; +use Google\Cloud\Core\LongRunning\LongRunningClientConnection; use Google\Cloud\Core\LongRunning\LongRunningOperation; -use Google\Cloud\Core\LongRunning\LROTrait; -use Google\Cloud\Core\ValidateTrait; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Core\OptionsValidator; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstanceConfigOperationsRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstanceConfigsRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstancesRequest; use Google\Cloud\Spanner\Admin\Instance\V1\ReplicaInfo; use Google\Cloud\Spanner\Batch\BatchClient; -use Google\Cloud\Spanner\Connection\Grpc; -use Google\Cloud\Spanner\Connection\LongRunningConnection; +use Google\Cloud\Spanner\Middleware\SpannerMiddleware; use Google\Cloud\Spanner\Session\SessionPoolInterface; -use Google\Cloud\Spanner\V1\SpannerClient as GapicSpannerClient; +use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient; use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; +use Google\LongRunning\Operation as OperationProto; +use Google\Protobuf\Duration; use Psr\Cache\CacheItemPoolInterface; -use Psr\Http\StreamInterface; +use Psr\Http\Message\StreamInterface; /** * Cloud Spanner is a highly scalable, transactional, managed, NewSQL @@ -57,7 +65,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * ``` * * ``` @@ -68,14 +76,15 @@ * // `9010` is used as an example only. * putenv('SPANNER_EMULATOR_HOST=localhost:9010'); * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * ``` * * ``` * use Google\Cloud\Spanner\SpannerClient; * use Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type as ReplicaType; * - * $directedOptions = [ + * $config = [ + * 'projectId' => 'my-project', * 'directedReadOptions' => [ * 'includeReplicas' => [ * 'replicaSelections' => [ @@ -88,56 +97,41 @@ * ] * ] * ]; - * $spanner = new SpannerClient($directedOptions); + * $spanner = new SpannerClient($config); * ``` * * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $config = ['routeToLeader' => false]; + * $config = ['projectId' => 'my-project', 'routeToLeader' => false]; * $spanner = new SpannerClient($config); * ``` - * - * @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 { - use ArrayTrait; - use ClientTrait; - use LROTrait; - use ValidateTrait; + use DetectProjectIdTrait; + use ClientOptionsTrait; + use EmulatorTrait; + use RequestTrait; const VERSION = '1.106.0'; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data'; const ADMIN_SCOPE = 'https://www.googleapis.com/auth/spanner.admin'; + private GapicSpannerClient $spannerClient; + private InstanceAdminClient $instanceAdminClient; + private DatabaseAdminClient $databaseAdminClient; + private Serializer $serializer; /** - * @var Connection\ConnectionInterface - * @internal + * @var string */ - protected $connection; - - /** - * @var bool - */ - private $returnInt64AsObject; - - /** - * @var array - */ - private $directedReadOptions; + private $projectId; + private string $projectName; + private bool $returnInt64AsObject; + private array $directedReadOptions; + private bool $routeToLeader; + private array $defaultQueryOptions; /** * @var int @@ -148,71 +142,23 @@ class SpannerClient * Create a Spanner client. Please note that this client requires * [the gRPC extension](https://cloud.google.com/php/grpc). * - * @param array $config [optional] { + * @param array $options { * Configuration Options. * + * @type string $projectId The Google Cloud project ID. * @type string $apiEndpoint A hostname with optional port to use in * place of the service's default endpoint. - * @type string $projectId The project ID from the Google Developer's - * Console. - * @type CacheItemPoolInterface $authCache A cache for storing access + * @type CacheItemPoolInterface $credentialsConfig.authCache A cache for storing access * tokens. **Defaults to** a simple in memory implementation. - * @type array $authCacheOptions Cache configuration options. - * @type callable $authHttpHandler A handler used to deliver Psr7 + * @type array $credentialsConfig.authCacheOptions Cache configuration options. + * @type callable $credentialsConfig.authHttpHandler A handler used to deliver Psr7 * requests specifically for authentication. - * @type FetchAuthTokenInterface $credentialsFetcher A credentials - * fetcher instance. - * @type callable $httpHandler A handler used to deliver Psr7 requests. + * @type callable $transportConfig.rest.httpHandler A handler used to deliver Psr7 requests. * Only valid for requests sent over REST. - * @type array $keyFile [DEPRECATED] - * @deprecated This option is being deprecated because of a potential security risk. - * This option does not validate the credential configuration. The security - * risk occurs when a credential configuration is accepted from a source - * that is not under your control and used without validation on your side. - * If you know that you will be loading credential configurations of a - * specific type, it is recommended to create the credentials directly and - * configure them using the `credentialsFetcher` option instead. - * ``` - * use Google\Auth\Credentials\ServiceAccountCredentials; - * $credentialsFetcher = new ServiceAccountCredentials($scopes, $json); - * $creds = new SpannerClient(['credentialsFetcher' => $creds]); - * ``` - * This will ensure that an unexpected credential type with potential for - * malicious intent is not loaded unintentionally. You might still have to do - * validation for certain credential types. - * If you are loading your credential configuration from an untrusted source and have - * not mitigated the risks (e.g. by validating the configuration yourself), make - * these changes as soon as possible to prevent security risks to your environment. - * Regardless of the method used, it is always your responsibility to validate - * configurations received from external sources. - * @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials - * @type string $keyFilePath [DEPRECATED] - * @deprecated This option is being deprecated because of a potential security risk. - * This option does not validate the credential configuration. The security - * risk occurs when a credential configuration is accepted from a source - * that is not under your control and used without validation on your side. - * If you know that you will be loading credential configurations of a - * specific type, it is recommended to create the credentials directly and - * configure them using the `credentialsFetcher` option instead. - * ``` - * use Google\Auth\Credentials\ServiceAccountCredentials; - * $credentialsFetcher = new ServiceAccountCredentials($scopes, $json); - * $creds = new SpannerClient(['credentialsFetcher' => $creds]); - * ``` - * This will ensure that an unexpected credential type with potential for - * malicious intent is not loaded unintentionally. You might still have to do - * validation for certain credential types. - * If you are loading your credential configuration from an untrusted source and have - * not mitigated the risks (e.g. by validating the configuration yourself), make - * these changes as soon as possible to prevent security risks to your environment. - * Regardless of the method used, it is always your responsibility to validate - * configurations received from external sources. - * @see https://cloud.google.com/docs/authentication/external/externally-sourced-credentials - * @type float $requestTimeout Seconds to wait before timing out the - * request. **Defaults to** `0` with REST and `60` with gRPC. - * @type int $retries Number of retries for a failed request. Used only - * with default backoff strategy. **Defaults to** `3`. - * @type array $scopes Scopes to be used for the request. + * @type string|array|FetchAuthTokenInterface|CredentialsWrapper $credentials + * The credentials to be used by the client to authorize API calls. + * @type array $credentialsConfig.scopes Scopes to be used for the request. + * @type string $credentialsConfig.quotaProject Specifies a user project to bill for * @type string $quotaProject Specifies a user project to bill for * access charges associated with the request. * @type bool $returnInt64AsObject If true, 64 bit integers will be @@ -233,10 +179,6 @@ class SpannerClient * query execution. Executing a SQL statement with an invalid * optimizer version will fail with a syntax error * (`INVALID_ARGUMENT`) status. - * @type bool $useDiscreteBackoffs `false`: use default backoff strategy - * (retry every failed request up to `retries` times). - * `true`: use discrete backoff settings based on called method name. - * **Defaults to** `false`. * @type array $directedReadOptions Directed read options. * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} * If using the `replicaSelection::type` setting, utilize the constants available in @@ -250,83 +192,79 @@ class SpannerClient * } * @throws GoogleException If the gRPC extension is not enabled. */ - public function __construct(array $config = []) + public function __construct(array $options = []) { $emulatorHost = getenv('SPANNER_EMULATOR_HOST'); $this->requireGrpc(); - $config += [ - 'scopes' => [ - self::FULL_CONTROL_SCOPE, - self::ADMIN_SCOPE - ], + $scopes = [self::FULL_CONTROL_SCOPE, self::ADMIN_SCOPE]; + $options += [ 'returnInt64AsObject' => false, 'projectIdRequired' => true, 'hasEmulator' => (bool) $emulatorHost, 'emulatorHost' => $emulatorHost, 'queryOptions' => [], + 'directedReadOptions' => [], 'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED, + 'routeToLeader' => true, ]; - if (!empty($config['useDiscreteBackoffs'])) { - $config = array_merge_recursive($config, [ - 'retries' => 0, - 'grpcOptions' => [ - 'retrySettings' => [], - ], - ]); + $this->returnInt64AsObject = $options['returnInt64AsObject']; + $this->directedReadOptions = $options['directedReadOptions']; + $this->routeToLeader = $options['routeToLeader']; + $this->defaultQueryOptions = $options['queryOptions']; + $this->isolationLevel = $options['isolationLevel']; + + // Configure GAPIC client options + $options = $this->buildClientOptions($options); + if (isset($options['credentialsConfig']['scopes'])) { + $options['credentialsConfig']['scopes'] = array_merge( + $options['credentialsConfig']['scopes'], + $scopes + ); + } else { + $options['credentialsConfig']['scopes'] = $scopes; } - $this->connection = new Grpc($this->configureAuthentication($config)); - $this->returnInt64AsObject = $config['returnInt64AsObject']; - - $this->setLroProperties(new LongRunningConnection($this->connection), [ - [ - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', - 'callable' => function ($instance) { - $name = InstanceAdminClient::parseName($instance['name'])['instance']; - return $this->instance($name, $instance); - } - ], [ - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata', - 'callable' => function ($database) { - $databaseNameComponents = DatabaseAdminClient::parseName($database['name']); - $instanceName = $databaseNameComponents['instance']; - $databaseName = $databaseNameComponents['database']; - - $instance = $this->instance($instanceName); - return $instance->database($databaseName); - } - ], [ - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.RestoreDatabaseMetadata', - 'callable' => function ($database) { - $databaseNameComponents = DatabaseAdminClient::parseName($database['name']); - $instanceName = $databaseNameComponents['instance']; - $databaseName = $databaseNameComponents['database']; - - $instance = $this->instance($instanceName); - return $instance->database($databaseName); - } - ], [ - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata', - 'callable' => function ($instance) { - $name = InstanceAdminClient::parseName($instance['name'])['instance']; - return $this->instance($name, $instance); - } - ], [ - 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateBackupMetadata', - 'callable' => function ($backup) { - $backupNameComponents = DatabaseAdminClient::parseName($backup['name']); - $instanceName = $backupNameComponents['instance']; - - $instance = $this->instance($instanceName); - return $instance->backup($backup['name'], $backup); - } - ] - ]); - - $this->directedReadOptions = $config['directedReadOptions'] ?? []; - $this->isolationLevel = $config['isolationLevel']; + if ($emulatorHost) { + $emulatorConfig = $this->emulatorGapicConfig($emulatorHost); + $options = array_merge( + $options, + $emulatorConfig + ); + } else { + $options['credentials'] = $this->createCredentialsWrapper( + $options['credentials'], + $options['credentialsConfig'], + $options['universeDomain'] + ); + } + $this->projectId = $this->detectProjectId($options); + $this->serializer = new Serializer(); + $this->optionsValidator = new OptionsValidator($this->serializer); + + // Adds some defaults + // gccl needs to be present for handwritten clients + $clientOptions = $options += [ + 'libName' => 'gccl', + 'serializer' => $this->serializer, + ]; + $this->spannerClient = $options['gapicSpannerClient'] ?? new GapicSpannerClient($clientOptions); + $this->instanceAdminClient = $options['gapicSpannerInstanceAdminClient'] + ?? new InstanceAdminClient($clientOptions); + $this->databaseAdminClient = $options['gapicSpannerDatabaseAdminClient'] + ?? new DatabaseAdminClient($clientOptions); + + // Add the SpannerMiddleware, which wraps API Exceptions, and adds + // Resource Prefix and LAR headers + $middleware = function (MiddlewareInterface $handler) { + return new SpannerMiddleware($handler); + }; + $this->spannerClient->addMiddleware($middleware); + $this->instanceAdminClient->addMiddleware($middleware); + $this->databaseAdminClient->addMiddleware($middleware); + + $this->projectName = InstanceAdminClient::projectName($this->projectId); } /** @@ -355,11 +293,16 @@ public function __construct(array $config = []) * } * @return BatchClient */ - public function batch($instanceId, $databaseId, array $options = []) + public function batch($instanceId, $databaseId, array $options = []): BatchClient { $operation = new Operation( - $this->connection, - $this->returnInt64AsObject + $this->spannerClient, + $this->serializer, + [ + 'routeToLeader' => $this->routeToLeader, + 'defaultQueryOptions' => $this->defaultQueryOptions, + 'returnInt64AsObject' => $this->returnInt64AsObject, + ] ); return new BatchClient( @@ -426,11 +369,15 @@ public function batch($instanceId, $databaseId, array $options = []) * @type bool $validateOnly An option to validate, but not actually execute, the request, and provide the same * response. **Defaults to** `false`. * } - * @return LongRunningOperation + * @return LongRunningOperation * @throws ValidationException */ - public function createInstanceConfiguration(InstanceConfiguration $baseConfig, $name, array $replicas, array $options = []) - { + public function createInstanceConfiguration( + InstanceConfiguration $baseConfig, + string $name, + array $replicas, + array $options = [] + ): LongRunningOperation { $config = $this->instanceConfiguration($name); return $config->create($baseConfig, $replicas, $options); } @@ -459,22 +406,28 @@ public function createInstanceConfiguration(InstanceConfiguration $baseConfig, $ * } * @return ItemIterator */ - public function instanceConfigurations(array $options = []) + public function instanceConfigurations(array $options = []): ItemIterator { - $resultLimit = $this->pluck('resultLimit', $options, false) ?: 0; - - return new ItemIterator( - new PageIterator( - function (array $config) { - return $this->instanceConfiguration($config['name'], $config); - }, - [$this->connection, 'listInstanceConfigs'], - ['projectName' => InstanceAdminClient::projectName($this->projectId)] + $options, - [ - 'itemsKey' => 'instanceConfigs', - 'resultLimit' => $resultLimit - ] - ) + /** + * @var ListInstanceConfigsRequest $listInstanceConfigs + * @var array $callOptions + */ + [$listInstanceConfigs, $callOptions] = $this->validateOptions( + $options, + new ListInstanceConfigsRequest(), + CallOptions::class + ); + $listInstanceConfigs->setParent($this->projectName); + + return $this->buildListItemsIterator( + [$this->instanceAdminClient, 'listInstanceConfigs'], + $listInstanceConfigs, + $callOptions + ['resource-prefix' => $this->projectName], + function (array $config) { + return $this->instanceConfiguration($config['name'], $config); + }, + 'instanceConfigs', + $this->pluck('resultLimit', $options, false) ); } @@ -497,17 +450,17 @@ function (array $config) { * @codingStandardsIgnoreEnd * * @param string $name The Configuration name. - * @param array $config [optional] The configuration details. + * @param array $info [optional] The configuration details. * @return InstanceConfiguration */ - public function instanceConfiguration($name, array $options = []) + public function instanceConfiguration($name, array $info = []): InstanceConfiguration { return new InstanceConfiguration( - $this->connection, + $this->instanceAdminClient, + $this->serializer, $this->projectId, $name, - $options, - $this->lroConnection + ['instanceConfig' => $info] ); } @@ -537,21 +490,37 @@ public function instanceConfiguration($name, array $options = []) * * @return ItemIterator */ - public function instanceConfigOperations(array $options = []) + public function instanceConfigOperations(array $options = []): ItemIterator { - $resultLimit = $this->pluck('resultLimit', $options, false); - return new ItemIterator( - new PageIterator( - function (array $operation) { - return $this->resumeOperation($operation['name'], $operation); - }, - [$this->connection, 'listInstanceConfigOperations'], - ['projectName' => InstanceAdminClient::projectName($this->projectId)] + $options, - [ - 'itemsKey' => 'operations', - 'resultLimit' => $resultLimit - ] - ) + /** + * @var ListInstanceConfigOperationsRequest $listInstanceConfigOperations + * @var array $callOptions + */ + [$listInstanceConfigOperations, $callOptions] = $this->validateOptions( + $options, + new ListInstanceConfigOperationsRequest(), + CallOptions::class, + ); + $listInstanceConfigOperations->setParent($this->projectName); + + return $this->buildLongRunningIterator( + [$this->instanceAdminClient, 'listInstanceConfigOperations'], + $listInstanceConfigOperations, + $callOptions + ['resource-prefix' => $this->projectName], + function (OperationProto $operation) { + return new LongRunningOperation( + new LongRunningClientConnection($this->databaseAdminClient, $this->serializer), + $operation->getName(), + [ + 'type.googleapis.com/google.spanner.admin.instance.v1.ListInstanceConfigMetadata' => + fn (InstanceConfig $config) => $this->instanceConfiguration( + $config->getName(), + $this->handleResponse($config) + ), + ], + $this->handleResponse($operation) + ); + }, ); } @@ -576,11 +545,14 @@ function (array $operation) { * @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 + * @return LongRunningOperation * @codingStandardsIgnoreEnd */ - public function createInstance(InstanceConfiguration $config, $name, array $options = []) - { + public function createInstance( + InstanceConfiguration $config, + string $name, + array $options = [] + ): LongRunningOperation { $instance = $this->instance($name); return $instance->create($config, $options); } @@ -596,20 +568,23 @@ public function createInstance(InstanceConfiguration $config, $name, array $opti * @param string $name The instance name * @return Instance */ - public function instance($name, array $instance = []) + public function instance(string $name, array $instance = []): Instance { return new Instance( - $this->connection, - $this->lroConnection, - $this->lroCallables, + $this->spannerClient, + $this->instanceAdminClient, + $this->databaseAdminClient, + $this->serializer, $this->projectId, $name, - $this->returnInt64AsObject, - $instance, [ 'directedReadOptions' => $this->directedReadOptions, - 'isolationLevel' => $this->isolationLevel - ] + 'routeToLeader' => $this->routeToLeader, + 'defaultQueryOptions' => $this->defaultQueryOptions, + 'returnInt64AsObject' => $this->returnInt64AsObject, + 'isolationLevel' => $this->isolationLevel, + 'instance' => $instance, + ], ); } @@ -639,26 +614,32 @@ public function instance($name, array $instance = []) * } * @return ItemIterator */ - public function instances(array $options = []) + public function instances(array $options = []): ItemIterator { $options += [ - 'filter' => null + 'filter' => '', + 'parent' => $this->projectName ]; + /** + * @var ListInstancesRequest $listInstances + * @var array $callOptions + */ + [$listInstances, $callOptions] = $this->validateOptions( + $options, + new ListInstancesRequest(), + CallOptions::class, + ); - $resultLimit = $this->pluck('resultLimit', $options, false); - return new ItemIterator( - new PageIterator( - function (array $instance) { - $name = InstanceAdminClient::parseName($instance['name'])['instance']; - return $this->instance($name, $instance); - }, - [$this->connection, 'listInstances'], - ['projectName' => InstanceAdminClient::projectName($this->projectId)] + $options, - [ - 'itemsKey' => 'instances', - 'resultLimit' => $resultLimit - ] - ) + return $this->buildListItemsIterator( + [$this->instanceAdminClient, 'listInstances'], + $listInstances, + $callOptions + ['resource-prefix' => $this->projectName], + function (array $instance) { + $name = InstanceAdminClient::parseName($instance['name'])['instance']; + return $this->instance($name, $instance); + }, + 'instances', + $this->pluck('resultLimit', $options, false) ); } @@ -689,7 +670,7 @@ function (array $instance) { * } * @return Database */ - public function connect($instance, $name, array $options = []) + public function connect(Instance|string $instance, string $name, array $options = []): Database { if (is_string($instance)) { $instance = $this->instance($instance); @@ -722,7 +703,7 @@ public function connect($instance, $name, array $options = []) * } * @return KeySet */ - public function keySet(array $options = []) + public function keySet(array $options = []): KeySet { return new KeySet($options); } @@ -759,7 +740,7 @@ public function keySet(array $options = []) * } * @return KeyRange */ - public function keyRange(array $options = []) + public function keyRange(array $options = []): KeyRange { return new KeyRange($options); } @@ -775,7 +756,7 @@ public function keyRange(array $options = []) * @param StreamInterface|string|resource $bytes The bytes value. * @return Bytes */ - public function bytes($bytes) + public function bytes(mixed $bytes): Bytes { return new Bytes($bytes); } @@ -791,7 +772,7 @@ public function bytes($bytes) * @param \DateTimeInterface $date The date value. * @return Date */ - public function date(\DateTimeInterface $date) + public function date(\DateTimeInterface $date): Date { return new Date($date); } @@ -805,10 +786,10 @@ public function date(\DateTimeInterface $date) * ``` * * @param \DateTimeInterface $timestamp The timestamp value. - * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp. + * @param int|null $nanoSeconds The number of nanoseconds in the timestamp. * @return Timestamp */ - public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null) + public function timestamp(\DateTimeInterface $timestamp, int|null $nanoSeconds = null): Timestamp { return new Timestamp($timestamp, $nanoSeconds); } @@ -832,7 +813,7 @@ public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null) * @return Numeric * @throws \InvalidArgumentException */ - public function numeric($value) + public function numeric(string|int|float $value): Numeric { return new Numeric($value); } @@ -853,7 +834,7 @@ public function numeric($value) * @param string|int|float|null $value The PgNumeric value. * @return PgNumeric */ - public function pgNumeric($value) + public function pgNumeric(string|int|float|null $value): PgNumeric { return new PgNumeric($value); } @@ -868,7 +849,7 @@ public function pgNumeric($value) * $pgJsonb = $spanner->pgJsonb('{}'); * ``` */ - public function pgJsonb($value) + public function pgJsonb(string|array|\JsonSerializable|null $value): PgJsonb { return new PgJsonb($value); } @@ -883,7 +864,7 @@ public function pgJsonb($value) * $pgOid = $spanner->pgOid('123'); * ``` */ - public function pgOid($value) + public function pgOid(string|null $value): PgOid { return new PgOid($value); } @@ -900,7 +881,7 @@ public function pgOid($value) * @param string $value * @return Int64 */ - public function int64($value) + public function int64(string $value): Int64 { return new Int64($value); } @@ -918,9 +899,9 @@ public function int64($value) * **Defaults to** `0`. * @return Duration */ - public function duration($seconds, $nanos = 0) + public function duration(int $seconds, int $nanos = 0): Duration { - return new Duration($seconds, $nanos); + return new Duration(['seconds' => $seconds, 'nanos' => $nanos]); } /** @@ -937,8 +918,35 @@ public function duration($seconds, $nanos = 0) * * @return CommitTimestamp */ - public function commitTimestamp() + public function commitTimestamp(): CommitTimestamp { return new CommitTimestamp(); } + + /** + * Throw an exception if the gRPC extension is not loaded. + * + * @throws GoogleException + */ + private function requireGrpc() + { + if (!$this->isGrpcLoaded()) { + throw new GoogleException( + 'The requested client requires the gRPC extension. ' + . 'Please see https://cloud.google.com/php/grpc for installation ' + . 'instructions.' + ); + } + } + + /** + * Abstract the checking of the grpc extension for unit testing. + * + * @codeCoverageIgnore + * @return bool + */ + private function isGrpcLoaded() + { + return extension_loaded('grpc'); + } } diff --git a/Spanner/src/StructType.php b/Spanner/src/StructType.php index 0f5fcf756f92..6be43550ef30 100644 --- a/Spanner/src/StructType.php +++ b/Spanner/src/StructType.php @@ -26,7 +26,7 @@ * use Google\Cloud\Spanner\Database; * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $database = $spanner->connect('my-instance', 'my-database'); * * $res = $database->execute('SELECT @userStruct.firstName, @userStruct.lastName', [ @@ -42,16 +42,12 @@ * ->add('lastName', Database::TYPE_STRING) * ] * ])->rows()->current(); - * - * $fullName = $res['firstName'] . ' ' . $res['lastName']; // `John Testuser` + * $fullName = $res['firstName'] . ' ' . $res['lastName']; // `John Testuser`; * ``` */ class StructType { - /** - * @var array - */ - private $fields = []; + private array $fields = []; /** * Example: @@ -127,7 +123,7 @@ public function __construct(array $fields = []) * ``` * * @param string|null $name The field name. - * @param int|ArrayType|StructType $type $type A value type code or nested + * @param int|string|ArrayType|StructType $type $type A value type code or nested * struct or array definition. Accepted integer values are defined as * constants on {@see \Google\Cloud\Spanner\Database}, and are as * follows: `Database::TYPE_BOOL`, `Database::TYPE_INT64`, @@ -138,7 +134,7 @@ public function __construct(array $fields = []) * definitions. * @throws \InvalidArgumentException If an invalid type is provided. */ - public function add($name, $type) + public function add(string|null $name, int|string|ArrayType|StructType $type): StructType { $invalidIntTypes = [ Database::TYPE_STRUCT, @@ -185,7 +181,7 @@ public function add($name, $type) * $structType->addUnnamed(Database::TYPE_STRING); * ``` * - * @param int|ArrayType|StructType $type $type A value type code or nested + * @param int|string|ArrayType|StructType $type $type A value type code or nested * struct or array definition. Accepted integer values are defined as * constants on {@see \Google\Cloud\Spanner\Database}, and are as * follows: `Database::TYPE_BOOL`, `Database::TYPE_INT64`, @@ -196,7 +192,7 @@ public function add($name, $type) * definitions. * @throws \InvalidArgumentException If an invalid type is provided. */ - public function addUnnamed($type) + public function addUnnamed(int|string|ArrayType|StructType $type): StructType { return $this->add(null, $type); } @@ -208,7 +204,7 @@ public function addUnnamed($type) * @return array[] An array containing a field definition. Each field * is of form `[(string|null) $name, (int) $type, (ArrayType|StructType|null) $child]`. */ - public function fields() + public function fields(): array { return $this->fields; } diff --git a/Spanner/src/StructValue.php b/Spanner/src/StructValue.php index 7cde1caffa00..cf00b3b90eca 100644 --- a/Spanner/src/StructValue.php +++ b/Spanner/src/StructValue.php @@ -40,7 +40,7 @@ * use Google\Cloud\Spanner\StructType; * use Google\Cloud\Spanner\StructValue; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * $database = $spanner->connect('my-instance', 'my-database'); * * $res = $database->execute('SELECT * FROM UNNEST(ARRAY(SELECT @structParam))', [ @@ -99,7 +99,7 @@ public function __construct(array $values = []) * @return StructValue The current instance, for chaining additional struct * values. */ - public function add($name, $value) + public function add(string|null $name, mixed $value): StructValue { $this->values[] = [ 'name' => $name, @@ -121,7 +121,7 @@ public function add($name, $value) * @return StructValue The current instance, for chaining additional struct * values. */ - public function addUnnamed($value) + public function addUnnamed(mixed $value): StructValue { return $this->add(null, $value); } @@ -132,7 +132,7 @@ public function addUnnamed($value) * @access private * @return array[] */ - public function values() + public function values(): array { return $this->values; } diff --git a/Spanner/src/Timestamp.php b/Spanner/src/Timestamp.php index 01b239881bb4..95f70fda6cb0 100644 --- a/Spanner/src/Timestamp.php +++ b/Spanner/src/Timestamp.php @@ -32,7 +32,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $timestamp = $spanner->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')); * ``` @@ -54,7 +54,7 @@ class Timestamp extends CoreTimestamp implements ValueInterface * * @return int */ - public function type() + public function type(): int { return Database::TYPE_TIMESTAMP; } diff --git a/Spanner/src/Transaction.php b/Spanner/src/Transaction.php index f72e554f3fb1..dbb11e003be0 100644 --- a/Spanner/src/Transaction.php +++ b/Spanner/src/Transaction.php @@ -21,6 +21,9 @@ use Google\Cloud\Core\Exception\AbortedException; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\V1\RequestOptions; +use Google\Cloud\Spanner\V1\TransactionOptions; +use Google\Protobuf\Duration; /** * Manages interaction with Cloud Spanner inside a Transaction. @@ -47,7 +50,7 @@ * ``` * use Google\Cloud\Spanner\SpannerClient; * - * $spanner = new SpannerClient(); + * $spanner = new SpannerClient(['projectId' => 'my-project']); * * $database = $spanner->connect('my-instance', 'my-database'); * @@ -68,69 +71,58 @@ class Transaction implements TransactionalReadInterface use MutationTrait; use TransactionalReadTrait; - /** - * @var CommitStats - */ - private $commitStats = []; - - /** - * @var array - */ - private $mutations = []; - - /** - * @var bool - */ - private $isRetry = false; - - private ValueMapper $mapper; + private array $commitStats = []; + private array $mutations = []; + private bool $isRetry; + private array|RequestOptions $requestOptions; /** * @param Operation $operation The Operation instance. * @param Session $session The session to use for spanner interactions. - * @param string $transactionId [optional] The Transaction ID. If no ID is - * provided, the Transaction will be a Single-Use Transaction. - * @param bool $isRetry Whether the transaction will automatically retry or not. - * @param string $tag A transaction tag. Requests made using this transaction will - * use this as the transaction tag. - * @param array $options [optional] { + * @param string $transactionId The Transaction ID. If no ID is provided, the Transaction will + * be a Single-Use Transaction. + * @param array $options { * Configuration Options. * - * @type array $begin The begin Transaction options. - * [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions) - * @type int $isolationLevel level of Isolation for this transaction instance - * **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED + * @type bool $isRetry Whether the transaction will automatically retry or not. + * @type string $tag A transaction tag. Requests made using this transaction will + * use this as the transaction tag. + * @type array $begin The begin Transaction options, for using inline begin transactions. + * See {@see V1\TransactionOptions}. + * @type array $singleUse The singleUse Transaction options. See {@see V1\TransactionOptions}. + * @type array $requestOptions See {@see V1\RequestOptions}. + * @type array $transactionOptions See {@see V1\TransactionOptions}. * } * @param ValueMapper $mapper Consumed internally for properly map mutation data. * @throws \InvalidArgumentException if a tag is specified on a single-use transaction. */ public function __construct( - Operation $operation, - Session $session, - $transactionId = null, - $isRetry = false, - $tag = null, - $options = [], - $mapper = null + private Operation $operation, + private Session $session, + private string|null $transactionId = null, + array $options = [], + private ValueMapper|null $mapper = null ) { - $this->operation = $operation; - $this->session = $session; - $this->transactionId = $transactionId; - $this->isRetry = $isRetry; - $this->type = ($transactionId || isset($options['begin'])) ? self::TYPE_PRE_ALLOCATED : self::TYPE_SINGLE_USE; - if ($this->type == self::TYPE_SINGLE_USE && isset($tag)) { + if ($this->type == self::TYPE_SINGLE_USE && isset($options['tag'])) { throw new \InvalidArgumentException( 'Cannot set a transaction tag on a single-use transaction.' ); } - $this->tag = $tag; $this->context = SessionPoolInterface::CONTEXT_READWRITE; - $this->options = $options; + $this->tag = $options['tag'] ?? null; + $this->isRetry = $options['isRetry'] ?? false; + $this->transactionSelector = array_intersect_key( + (array) $options, + array_flip(['singleUse', 'begin']) + ); + $this->requestOptions = $options['requestOptions'] ?? []; + $this->transactionOptions = $options['transactionOptions'] ?? new TransactionOptions(); + if (!is_null($mapper)) { $this->mapper = $mapper; } @@ -149,7 +141,7 @@ public function __construct( * * @return array The commit stats */ - public function getCommitStats() + public function getCommitStats(): array { return $this->commitStats; } @@ -244,7 +236,7 @@ public function getCommitStats() * @return int The number of rows modified. * @throws ValidationException */ - public function executeUpdate($sql, array $options = []) + public function executeUpdate(string $sql, array $options = []): int { if (isset($options['transaction']['begin']['excludeTxnFromChangeStreams'])) { throw new ValidationException( @@ -257,7 +249,7 @@ public function executeUpdate($sql, array $options = []) if (isset($options['transaction']['begin']['readOnly'])) { // isolationLevel can only be used with read/write transactions throw new ValidationException( - 'The isolation level can only be applied to read/write transactions.' . + 'The isolation level can only be applied to read/write transactions. ' . 'Single use transactions are not read/write', ); } @@ -354,7 +346,7 @@ public function executeUpdate($sql, array $options = []) * @return BatchDmlResult * @throws \InvalidArgumentException If any statement is missing the `sql` key. */ - public function executeUpdateBatch(array $statements, array $options = []) + public function executeUpdateBatch(array $statements, array $options = []): BatchDmlResult { $options = $this->buildUpdateOptions($options); return $this->operation @@ -385,7 +377,7 @@ public function executeUpdateBatch(array $statements, array $options = []) * @param array $options [optional] Configuration Options. * @return void */ - public function rollback(array $options = []) + public function rollback(array $options = []): void { if ($this->state !== self::STATE_ACTIVE) { throw new \BadMethodCallException('The transaction cannot be rolled back because it is not active'); @@ -432,28 +424,27 @@ public function rollback(array $options = []) * Please note, the `requestTag` setting will be ignored as it is not supported for commit requests. * } * @return Timestamp The commit timestamp. - * @throws \BadMethodCall If the transaction is not active or already used. + * @throws \BadMethodCallException If the transaction is not active or already used. * @throws AbortedException If the commit is aborted for any reason. */ - public function commit(array $options = []) + public function commit(array $options = []): Timestamp { if ($this->state !== self::STATE_ACTIVE) { throw new \BadMethodCallException('The transaction cannot be committed because it is not active'); } // For commit, A transaction ID is mandatory for non-single-use transactions, - // and the `begin` option is not supported. - if (empty($this->transactionId) && isset($this->options['begin'])) { - // Since the begin option is not supported in commit, unset it. - unset($this->options['begin']); - - // A transaction ID is mandatory for non-single-use transactions. - if ($this->type !== self::TYPE_SINGLE_USE) { - // Execute the beginTransaction RPC. - $transaction = $this->operation->transaction($this->session, $this->options); - // Set the transaction ID of the current transaction. - $this->transactionId = $transaction->id(); - } + // and the `begin` option is not supported (because `begin` is only used in "inline begin transactions") + if (empty($this->transactionId) && isset($this->transactionSelector['begin'])) { + $operationTransactionOptions = array_filter([ + 'requestOptions' => $this->requestOptions, + 'transactionOptions' => $this->transactionOptions, + 'singleUse' => $this->transactionSelector['singleUse'] ?? null, + ]); + // Execute the beginTransaction RPC. + $transaction = $this->operation->transaction($this->session, $operationTransactionOptions); + // Set the transaction ID of the current transaction. + $this->transactionId = $transaction->id(); } if (!$this->singleUseState()) { @@ -476,6 +467,7 @@ public function commit(array $options = []) $t = $this->transactionOptions($options); + // @TODO find out what this is and clean it up $options[$t[1]] = $t[0]; $res = $this->operation->commitWithResponse($this->session, $this->pluck('mutations', $options), $options); @@ -499,7 +491,7 @@ public function commit(array $options = []) * * @return int */ - public function state() + public function state(): int { return $this->state; } @@ -522,7 +514,7 @@ public function state() * * @return bool */ - public function isRetry() + public function isRetry(): bool { return $this->isRetry; } @@ -548,14 +540,17 @@ private function buildUpdateOptions(array $options): array $this->seqno++; $options['transactionType'] = $this->context; - if (empty($this->transactionId) && isset($this->options['begin'])) { - $options['begin'] = $this->options['begin']; + if (empty($this->transactionId) && isset($this->transactionSelector['begin'])) { + $options['begin'] = $this->transactionSelector['begin']; } else { $options['transactionId'] = $this->transactionId; } + $selector = $this->transactionSelector($options); $options['transaction'] = $selector[0]; - return $this->addLarHeader($options); + $options['headers']['spanner-route-to-leader'] = ['true']; + + return $options; } } diff --git a/Spanner/src/TransactionConfigurationTrait.php b/Spanner/src/TransactionConfigurationTrait.php index 232002f3cdf0..49931399985c 100644 --- a/Spanner/src/TransactionConfigurationTrait.php +++ b/Spanner/src/TransactionConfigurationTrait.php @@ -17,12 +17,16 @@ namespace Google\Cloud\Spanner; -use Google\Cloud\Core\ArrayTrait; +use Google\ApiCore\ArrayTrait; use Google\Cloud\Spanner\Session\SessionPoolInterface; -use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode as ReadLockMode; +use Google\Cloud\Spanner\V1\TransactionOptions; +use Google\Cloud\Spanner\V1\TransactionOptions\PBReadOnly; +use Google\Protobuf\Duration; /** * Configure transaction selection for read, executeSql, rollback and commit. + * + * @internal */ trait TransactionConfigurationTrait { @@ -34,21 +38,23 @@ trait TransactionConfigurationTrait * Depending on given options, can be singleUse or begin, and can be read * or read/write. * + * @see V1\TransactionSelector + * * @param array $options call options. - * @param array $previous Previously given call options (for single-use snapshots). + * @param PBReadOnly $transactionLevelReadOnlyOptions Previously given call options (for single-use snapshots). * @return array [(array) transaction selector, (string) context] */ - private function transactionSelector(array &$options, array $previous = []) + private function transactionSelector(array &$options, ?PBReadOnly $transactionLevelReadOnlyOptions = null): array { $options += [ 'begin' => false, 'transactionType' => SessionPoolInterface::CONTEXT_READ, ]; - [$transactionOptions, $type, $context] = $this->transactionOptions($options, $previous); + [$transactionOptions, $type, $context] = $this->transactionOptions($options, $transactionLevelReadOnlyOptions); // TransactionSelector uses a different key name for singleUseTransaction - // and transactionId than transactionOptions, so we'll rewrite those here + // and transactionId than CommitRequest, so we'll rewrite those here // so transactionOptions works as expected for commitRequest. if ($type === 'singleUseTransaction') { @@ -64,57 +70,24 @@ private function transactionSelector(array &$options, array $previous = []) } /** - * Configure the DirectedReadOptions. + * Return transaction options based on given configuration options. * - * Request level DirectedReadOptions takes precedence over client level DirectedReadOptions. - * Client level DirectedReadOptions apply only to read-only and single-use transactions. + * @see V1\TransactionOptions * - * @param array $requestOptions Request level options. - * @param array $clientOptions Client level Directed Read Options. - * @return array - */ - private function configureDirectedReadOptions(array $requestOptions, array $clientOptions) - { - if (isset($requestOptions['directedReadOptions'])) { - return $requestOptions['directedReadOptions']; - } - - if (isset($requestOptions['transaction']['singleUse']) || ( - isset($requestOptions['transactionContext']) && - $requestOptions['transactionContext'] == SessionPoolInterface::CONTEXT_READ - ) || isset($requestOptions['transactionOptions']['readOnly']) - ) { - if (isset($clientOptions['includeReplicas'])) { - return ['includeReplicas' => $clientOptions['includeReplicas']]; - } elseif (isset($clientOptions['excludeReplicas'])) { - return ['excludeReplicas' => $clientOptions['excludeReplicas']]; - } - } - - return []; - } - - /** - * Return transaction options based on given configuration options. + * @param array $options call options * - * @param array $options call options. - * @param array $previous Previously given call options (for single-use snapshots). + * @param PBReadOnly $transactionLevelReadOnlyOptions 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 = []) + private function transactionOptions(array &$options, ?PBReadOnly $transactionLevelReadOnlyOptions = null): array { - $options += [ - 'begin' => false, - 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE, - 'transactionId' => null, - ]; + // @TODO: Remove $options being passed by reference $type = null; + $begin = $options['begin'] ?? []; + $context = $options['transactionType'] ?? SessionPoolInterface::CONTEXT_READWRITE; + $id = $options['transactionId'] ?? null; - $context = $this->pluck('transactionType', $options); - $id = $this->pluck('transactionId', $options); - - $begin = $this->pluck('begin', $options); if ($id === null) { if ($begin) { $type = 'begin'; @@ -128,10 +101,15 @@ private function transactionOptions(array &$options, array $previous = []) $type = 'transactionId'; $transactionOptions = $id; } elseif ($context === SessionPoolInterface::CONTEXT_READ) { - $transactionOptions = $this->configureSnapshotOptions($options, $previous); + $options += ['singleUse' => null]; + $transactionOptions = $this->configureReadOnlyTransactionOptions( + $options, + $transactionLevelReadOnlyOptions + ); } elseif ($context === SessionPoolInterface::CONTEXT_READWRITE) { - $transactionOptions = $this->configureTransactionOptions( - $type == 'begin' && is_array($begin) ? $begin : [] + $transactionOptions = $this->configureReadWriteTransactionOptions( + // TODO: Find out when $begin is a bool and fix it + $type == 'begin' && !is_bool($begin) ? $begin : [] ); } else { throw new \BadMethodCallException(sprintf( @@ -140,118 +118,181 @@ private function transactionOptions(array &$options, array $previous = []) )); } + // @TODO: Remove this once $options is no longer passed by reference + unset( + $options['begin'], + $options['transactionType'], + $options['transactionId'], + ); + + // For backwards compatibility - remove all PBReadOnly fields + // This was previously being done in configureReadOnlyTransactionOptions + // @TODO: Remove this once $options is no longer passed by reference + unset( + $options['returnReadTimestamp'], + $options['strong'], + $options['readTimestamp'], + $options['exactStaleness'], + $options['minReadTimestamp'], + $options['maxStaleness'], + ); + return [$transactionOptions, $type, $context]; } - private function configureTransactionOptions(array $options = []) + // Init readWrite options array with any necessary defaults for its nested options + private function initReadWriteTransactionOptions(): array { - $transactionOptions = [ - 'readWrite' => [] - ]; - - if (isset($options['excludeTxnFromChangeStreams'])) { - $transactionOptions['excludeTxnFromChangeStreams'] = $options['excludeTxnFromChangeStreams']; - } - - if (isset($options['isolationLevel'])) { - $transactionOptions['isolationLevel'] = $options['isolationLevel']; - } - - // Allow for proper configuring of the `readLockMode` if it's set as a base or nested option - if (isset($options['readLockMode'])) { - $transactionOptions['readWrite']['readLockMode'] = $options['readLockMode']; - } - - if (isset($options['readWrite']['readLockMode'])) { - $transactionOptions['readWrite']['readLockMode'] = $options['readWrite']['readLockMode']; - } + return ['readWrite' => []]; + } - return $transactionOptions; + private function configureReadWriteTransactionOptions(array|TransactionOptions $options): array + { + $excludeTxn = $options instanceof TransactionOptions + ? $options->getExcludeTxnFromChangeStreams() + : $options['excludeTxnFromChangeStreams'] ?? null; + $isolationLevel = $options instanceof TransactionOptions + ? $options->getIsolationLevel() + : $options['isolationLevel'] ?? null; + $readLockMode = $options instanceof TransactionOptions + ? $options->getReadWrite()->getReadLockMode() + : $options['readLockMode'] ?? $options['readWrite']['readLockMode'] ?? null; + return array_filter([ + 'excludeTxnFromChangeStreams' => $excludeTxn, + 'isolationLevel' => $isolationLevel, + 'readWrite' => array_filter(['readLockMode' => $readLockMode]), + ]) + $this->initReadWriteTransactionOptions(); } /** * Configure a Read-Only transaction. * - * @param array $options Configuration Options. - * @param array $previous Previously given call options (for single-use snapshots). + * @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. + * @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`. + * } + * @param PBReadOnly $transactionLevelReadOnlyOptions Previously given call options (for single-use snapshots). * @return array */ - private function configureSnapshotOptions(array &$options, array $previous = []) - { - $options += [ - 'singleUse' => false, - 'returnReadTimestamp' => null, - 'strong' => null, - 'readTimestamp' => null, - 'exactStaleness' => null, - 'minReadTimestamp' => null, - 'maxStaleness' => null, - ]; + private function configureReadOnlyTransactionOptions( + array $options, + ?PBReadOnly $transactionLevelReadOnlyOptions = null + ): array { + // select only the PBReadOnly fields from $options + $readOnly = array_intersect_key($options, array_flip([ + 'minReadTimestamp', + 'readTimestamp', + 'returnReadTimestamp', + 'exactStaleness', + 'maxStaleness', + 'strong' + ])); - $previousOptions = $previous['transactionOptions']['readOnly'] ?? []; + // Validate options types + if ($this->validateOptionType($readOnly, 'minReadTimestamp', Timestamp::class)) { + $readOnly['minReadTimestamp'] = $readOnly['minReadTimestamp']->formatAsString(); + } + + if ($this->validateOptionType($readOnly, 'readTimestamp', Timestamp::class)) { + $readOnly['readTimestamp'] = $readOnly['readTimestamp']->formatAsString(); + } + + $this->validateOptionType($readOnly, 'exactStaleness', Duration::class); + $this->validateOptionType($readOnly, 'maxStaleness', Duration::class); // These are only available in single-use transactions. - if (!$options['singleUse'] && ($options['maxStaleness'] || $options['minReadTimestamp'])) { + if (!($options['singleUse'] ?? false) + && (!empty($readOnly['maxStaleness']) || !empty($readOnly['minReadTimestamp'])) + ) { throw new \BadMethodCallException( 'maxStaleness and minReadTimestamp are only available in single-use transactions.' ); } - $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), - ]) + $previousOptions - ]; - - if (empty($transactionOptions['readOnly'])) { - $transactionOptions['readOnly']['strong'] = true; + if ($transactionLevelReadOnlyOptions && empty($readOnly)) { + $readOnly = $transactionLevelReadOnlyOptions; } - $timestampFields = [ - 'minReadTimestamp', - 'readTimestamp' - ]; + if (empty($readOnly)) { + $readOnly['strong'] = true; + } - $durationFields = [ - 'exactStaleness', - 'maxStaleness' - ]; + return ['readOnly' => $readOnly]; + } - foreach ($timestampFields as $tsf) { - if (isset($transactionOptions['readOnly'][$tsf]) && !isset($previousOptions[$tsf])) { - $field = $transactionOptions['readOnly'][$tsf]; - if (!($field instanceof Timestamp)) { - throw new \BadMethodCallException(sprintf( - 'Read Only Transaction Configuration Field %s must be an instance of `%s`.', - $tsf, - Timestamp::class - )); - } + /** + * Configure the DirectedReadOptions. + * + * Request level DirectedReadOptions takes precedence over client level DirectedReadOptions. + * Client level DirectedReadOptions apply only to read-only and single-use transactions. + * + * @param array $requestOptions Request level options. + * @param array $clientOptions Client level Directed Read Options. + * @return array + */ + private function configureDirectedReadOptions(array $requestOptions, array $clientOptions): array + { + if (isset($requestOptions['directedReadOptions'])) { + return $requestOptions['directedReadOptions']; + } - $transactionOptions['readOnly'][$tsf] = $field->formatAsString(); + if (isset($requestOptions['transaction']['singleUse']) || ( + ($requestOptions['transactionContext'] ?? null) == SessionPoolInterface::CONTEXT_READ + ) || isset($requestOptions['transactionOptions']['readOnly']) + ) { + if (isset($clientOptions['includeReplicas'])) { + return ['includeReplicas' => $clientOptions['includeReplicas']]; + } elseif (isset($clientOptions['excludeReplicas'])) { + return ['excludeReplicas' => $clientOptions['excludeReplicas']]; } } - foreach ($durationFields as $df) { - if (isset($transactionOptions['readOnly'][$df]) && !isset($previousOptions[$df])) { - $field = $transactionOptions['readOnly'][$df]; - if (!($field instanceof Duration)) { - throw new \BadMethodCallException(sprintf( - 'Read Only Transaction Configuration Field %s must be an instance of `%s`.', - $df, - Duration::class - )); - } + return []; + } - $transactionOptions['readOnly'][$df] = $field->get(); - } + /** + * @throws \BadMethodCallException + */ + private function validateOptionType(array $options, string $field, string $type): bool + { + if (!isset($options[$field])) { + return false; + } + + if (!$options[$field] instanceof $type) { + throw new \BadMethodCallException(sprintf( + 'Read Only Transaction Configuration Field %s must be an instance of `%s`.', + $field, + $type + )); } - return $transactionOptions; + return true; } } diff --git a/Spanner/src/TransactionalReadInterface.php b/Spanner/src/TransactionalReadInterface.php index b5a71ac4ea92..69f627323d6b 100644 --- a/Spanner/src/TransactionalReadInterface.php +++ b/Spanner/src/TransactionalReadInterface.php @@ -28,7 +28,6 @@ interface TransactionalReadInterface const STATE_ROLLED_BACK = 1; const STATE_COMMITTED = 2; const STATE_SINGLE_USE_USED = 3; - const TYPE_SINGLE_USE = 0; const TYPE_PRE_ALLOCATED = 1; @@ -39,7 +38,7 @@ interface TransactionalReadInterface * @param array $options [optional] Configuration options. * @return Result */ - public function execute($sql, array $options = []); + public function execute(string $sql, array $options = []): Result; /** * Lookup rows in a table. @@ -50,24 +49,24 @@ public function execute($sql, array $options = []); * @param array $options [optional] Configuration Options. * @return Result */ - public function read($table, KeySet $keySet, array $columns, array $options = []); + public function read(string $table, KeySet $keySet, array $columns, array $options = []): Result; /** * Retrieve the Transaction ID. * * @return string|null */ - public function id(); + public function id(): string|null; /** * Get the Transaction Type. * * @return int */ - public function type(); + public function type(): int; /** * Set the transaction ID. */ - public function setId(?string $transactionId); + public function setId(?string $transactionId): void; } diff --git a/Spanner/src/TransactionalReadTrait.php b/Spanner/src/TransactionalReadTrait.php index e070cc444cd3..9ec0d403f632 100644 --- a/Spanner/src/TransactionalReadTrait.php +++ b/Spanner/src/TransactionalReadTrait.php @@ -19,64 +19,34 @@ use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\V1\TransactionOptions; /** * Shared methods for reads inside a transaction. + * + * @internal */ trait TransactionalReadTrait { use TransactionConfigurationTrait; - use RequestHeaderTrait; - - /** - * @var Operation - */ - private $operation; - - /** - * @var Session - */ - private $session; + private Operation $operation; + private Session $session; + private string|null $transactionId; + private string $context; + private int $type; + private int $state = TransactionalReadInterface::STATE_ACTIVE; /** - * @var string + * @see V1\TransactionSelector */ - private $transactionId; - + private array $transactionSelector = []; /** - * @var string + * @see V1\TransactionOptions */ - private $context; - - /** - * @var int - */ - private $type; - - /** - * @var int - */ - private $state = 0; // TransactionalReadInterface::STATE_ACTIVE - - /** - * @var array - */ - private $options = []; - - /** - * @var int - */ - private $seqno = 1; - - /** - * @var string - */ - private $tag = null; - - /** - * @var array - */ - private $directedReadOptions = []; + private TransactionOptions $transactionOptions; + private int $seqno = 1; + private string|null $tag = null; + private array $directedReadOptions = []; /** * Run a query. @@ -272,40 +242,46 @@ trait TransactionalReadTrait * @codingStandardsIgnoreEnd * @return Result */ - public function execute($sql, array $options = []) + public function execute(string $sql, array $options = []): Result { $this->singleUseState(); $this->checkReadContext(); - if (empty($this->transactionId) && isset($this->options['begin'])) { - $options['begin'] = $this->options['begin']; + $executeSqlOptions = $options; + if (empty($this->transactionId) && isset($this->transactionSelector['begin'])) { + $executeSqlOptions['begin'] = $this->transactionSelector['begin']; } else { - $options['transactionId'] = $this->transactionId; + $executeSqlOptions['transactionId'] = $this->transactionId; } - $options['transactionType'] = $this->context; - $options['seqno'] = $this->seqno; + $executeSqlOptions['transactionType'] = $this->context; + $executeSqlOptions['seqno'] = $this->seqno; $this->seqno++; - $selector = $this->transactionSelector($options, $this->options); + $readOnly = $this->transactionOptions->getReadOnly(); + $selector = $this->transactionSelector($executeSqlOptions, $readOnly); - $options['transaction'] = $selector[0]; + $executeSqlOptions['transaction'] = $selector[0]; - unset($options['requestOptions']['transactionTag']); + unset($executeSqlOptions['requestOptions']['transactionTag']); if (isset($this->tag)) { - $options += [ + $executeSqlOptions += [ 'requestOptions' => [] ]; - $options['requestOptions']['transactionTag'] = $this->tag; + $executeSqlOptions['requestOptions']['transactionTag'] = $this->tag; } - $options['directedReadOptions'] = $this->configureDirectedReadOptions( - $options, + $executeSqlOptions['directedReadOptions'] = $this->configureDirectedReadOptions( + $executeSqlOptions, $this->directedReadOptions ?? [] ); - $options = $this->addLarHeader($options, true, $this->context); + // Unsetting the internal flag + unset($executeSqlOptions['singleUse']); + + $result = $this->operation->execute($this->session, $sql, $executeSqlOptions + [ + 'route-to-leader' => $this->context === SessionPoolInterface::CONTEXT_READWRITE + ]); - $result = $this->operation->execute($this->session, $sql, $options); if (empty($this->id()) && $result->transaction()) { $this->setId($result->transaction()->id()); } @@ -361,19 +337,20 @@ public function execute($sql, array $options = []) * } * @return Result */ - public function read($table, KeySet $keySet, array $columns, array $options = []) + public function read(string $table, KeySet $keySet, array $columns, array $options = []): Result { $this->singleUseState(); $this->checkReadContext(); - if (empty($this->transactionId) && isset($this->options['begin'])) { - $options['begin'] = $this->options['begin']; + if (empty($this->transactionId) && isset($this->transactionSelector['begin'])) { + $options['begin'] = $this->transactionSelector['begin']; } else { $options['transactionId'] = $this->transactionId; } + $options['transactionType'] = $this->context; - $options += $this->options; - $selector = $this->transactionSelector($options, $this->options); + $readOnly = $this->transactionOptions->getReadOnly(); + $selector = $this->transactionSelector($options, $readOnly); $options['transaction'] = $selector[0]; @@ -390,9 +367,9 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] $this->directedReadOptions ?? [] ); - $options = $this->addLarHeader($options, true, $this->context); - - $result = $this->operation->read($this->session, $table, $keySet, $columns, $options); + $result = $this->operation->read($this->session, $table, $keySet, $columns, $options + [ + 'route-to-leader' => $this->context === SessionPoolInterface::CONTEXT_READWRITE + ]); if (empty($this->id()) && $result->transaction()) { $this->setId($result->transaction()->id()); } @@ -409,7 +386,7 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] * * @return string|null */ - public function id() + public function id(): string|null { return $this->transactionId; } @@ -417,7 +394,7 @@ public function id() /** * Set the transaction ID. */ - public function setId(?string $transactionId) + public function setId(string|null $transactionId): void { $this->transactionId = $transactionId; } @@ -428,7 +405,7 @@ public function setId(?string $transactionId) * @access private * @return int */ - public function type() + public function type(): int { return $this->type; } @@ -439,7 +416,7 @@ public function type() * @access private * @return Session */ - public function session() + public function session(): Session { return $this->session; } @@ -450,7 +427,7 @@ public function session() * @return bool true if transaction is single use, false otherwise. * @throws \BadMethodCallException */ - private function singleUseState() + private function singleUseState(): bool { if ($this->type === self::TYPE_SINGLE_USE) { if ($this->state === self::STATE_SINGLE_USE_USED) { @@ -471,7 +448,7 @@ private function singleUseState() * * @throws \BadMethodCallException */ - private function checkReadContext() + private function checkReadContext(): void { 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/Spanner/src/TypeAnnotationInterface.php b/Spanner/src/TypeAnnotationInterface.php index 66f9db339760..33bdd8c7bf5e 100644 --- a/Spanner/src/TypeAnnotationInterface.php +++ b/Spanner/src/TypeAnnotationInterface.php @@ -27,5 +27,5 @@ interface TypeAnnotationInterface /** * @return int */ - public function typeAnnotation(); + public function typeAnnotation(): int; } diff --git a/Spanner/src/V1/Gapic/SpannerGapicClient.php b/Spanner/src/V1/Gapic/SpannerGapicClient.php deleted file mode 100644 index 3e9c5a931c6c..000000000000 --- a/Spanner/src/V1/Gapic/SpannerGapicClient.php +++ /dev/null @@ -1,2119 +0,0 @@ -databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * $sessionCount = 0; - * $response = $spannerClient->batchCreateSessions($formattedDatabase, $sessionCount); - * } 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 parseName method to extract the individual identifiers - * contained within formatted names that are returned by the API. - * - * @deprecated Please use the new service client {@see \Google\Cloud\Spanner\V1\Client\SpannerClient}. - */ -class SpannerGapicClient -{ - use GapicClientTrait; - - /** The name of the service. */ - const SERVICE_NAME = 'google.spanner.v1.Spanner'; - - /** - * The default address of the service. - * - * @deprecated SERVICE_ADDRESS_TEMPLATE should be used instead. - */ - const SERVICE_ADDRESS = 'spanner.googleapis.com'; - - /** The address template of the service. */ - private const SERVICE_ADDRESS_TEMPLATE = 'spanner.UNIVERSE_DOMAIN'; - - /** The default port of the service. */ - const DEFAULT_SERVICE_PORT = 443; - - /** The name of the code generator, to be included in the agent header. */ - const CODEGEN_NAME = 'gapic'; - - /** The default scopes required by the service. */ - public static $serviceScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.data', - ]; - - private static $databaseNameTemplate; - - private static $sessionNameTemplate; - - private static $pathTemplateMap; - - private static function getClientDefaults() - { - return [ - 'serviceName' => self::SERVICE_NAME, - 'apiEndpoint' => - self::SERVICE_ADDRESS . ':' . self::DEFAULT_SERVICE_PORT, - 'clientConfig' => - __DIR__ . '/../resources/spanner_client_config.json', - 'descriptorsConfigPath' => - __DIR__ . '/../resources/spanner_descriptor_config.php', - 'gcpApiConfigPath' => - __DIR__ . '/../resources/spanner_grpc_config.json', - 'credentialsConfig' => [ - 'defaultScopes' => self::$serviceScopes, - ], - 'transportConfig' => [ - 'rest' => [ - 'restClientConfigPath' => - __DIR__ . - '/../resources/spanner_rest_client_config.php', - ], - ], - ]; - } - - 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 getPathTemplateMap() - { - if (self::$pathTemplateMap == null) { - self::$pathTemplateMap = [ - 'database' => self::getDatabaseNameTemplate(), - 'session' => self::getSessionNameTemplate(), - ]; - } - - return self::$pathTemplateMap; - } - - /** - * Formats a string containing the fully-qualified path to represent a database - * resource. - * - * @param string $project - * @param string $instance - * @param string $database - * - * @return string The formatted database resource. - */ - public static function databaseName($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. - * - * @param string $project - * @param string $instance - * @param string $database - * @param string $session - * - * @return string The formatted session resource. - */ - public static function sessionName($project, $instance, $database, $session) - { - return self::getSessionNameTemplate()->render([ - 'project' => $project, - 'instance' => $instance, - 'database' => $database, - 'session' => $session, - ]); - } - - /** - * Parses a formatted name string and returns an associative array of the components in the name. - * The following name formats are supported: - * Template: Pattern - * - database: projects/{project}/instances/{instance}/databases/{database} - * - session: projects/{project}/instances/{instance}/databases/{database}/sessions/{session} - * - * The optional $template argument can be supplied to specify a particular pattern, - * and must match one of the templates listed above. If no $template argument is - * provided, or if the $template argument does not match one of the templates - * listed, then parseName will check each of the supported templates, and return - * the first match. - * - * @param string $formattedName The formatted name string - * @param string $template Optional name of template to match - * - * @return array An associative array from name component IDs to component values. - * - * @throws ValidationException If $formattedName could not be matched. - */ - public static function parseName($formattedName, $template = null) - { - $templateMap = self::getPathTemplateMap(); - if ($template) { - if (!isset($templateMap[$template])) { - throw new ValidationException( - "Template name $template does not exist" - ); - } - - return $templateMap[$template]->match($formattedName); - } - - foreach ($templateMap as $templateName => $pathTemplate) { - try { - return $pathTemplate->match($formattedName); - } catch (ValidationException $ex) { - // Swallow the exception to continue trying other path templates - } - } - - throw new ValidationException( - "Input did not match any known format. Input: $formattedName" - ); - } - - /** - * Constructor. - * - * @param array $options { - * Optional. Options for configuring the service API wrapper. - * - * @type string $apiEndpoint - * The address of the API remote host. May optionally include the port, formatted - * as ":". Default 'spanner.googleapis.com:443'. - * @type string|array|FetchAuthTokenInterface|CredentialsWrapper $credentials - * The credentials to be used by the client to authorize API calls. This option - * accepts either a path to a credentials file, or a decoded credentials file as a - * PHP array. - * *Advanced usage*: In addition, this option can also accept a pre-constructed - * {@see \Google\Auth\FetchAuthTokenInterface} object or - * {@see \Google\ApiCore\CredentialsWrapper} object. Note that when one of these - * objects are provided, any settings in $credentialsConfig will be ignored. - * @type array $credentialsConfig - * Options used to configure credentials, including auth token caching, for the - * client. For a full list of supporting configuration options, see - * {@see \Google\ApiCore\CredentialsWrapper::build()} . - * @type bool $disableRetries - * Determines whether or not retries defined by the client configuration should be - * disabled. Defaults to `false`. - * @type string|array $clientConfig - * Client method configuration, including retry settings. This option can be either - * a path to a JSON file, or a PHP array containing the decoded JSON data. By - * default this settings points to the default client config file, which is - * provided in the resources folder. - * @type string|TransportInterface $transport - * The transport used for executing network requests. May be either the string - * `rest` or `grpc`. Defaults to `grpc` if gRPC support is detected on the system. - * *Advanced usage*: Additionally, it is possible to pass in an already - * instantiated {@see \Google\ApiCore\Transport\TransportInterface} object. Note - * that when this object is provided, any settings in $transportConfig, and any - * $apiEndpoint setting, will be ignored. - * @type array $transportConfig - * Configuration options that will be used to construct the transport. Options for - * each supported transport type should be passed in a key for that transport. For - * example: - * $transportConfig = [ - * 'grpc' => [...], - * 'rest' => [...], - * ]; - * See the {@see \Google\ApiCore\Transport\GrpcTransport::build()} and - * {@see \Google\ApiCore\Transport\RestTransport::build()} methods for the - * supported options. - * @type callable $clientCertSource - * A callable which returns the client cert as a string. This can be used to - * provide a certificate and private key to the transport layer for mTLS. - * } - * - * @throws ValidationException - */ - public function __construct(array $options = []) - { - $clientOptions = $this->buildClientOptions($options); - $this->setClientOptions($clientOptions); - } - - /** - * Creates multiple new sessions. - * - * This API can be used to initialize a session cache on the clients. - * See https://goo.gl/TgSFN2 for best practices on session cache management. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedDatabase = $spannerClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * $sessionCount = 0; - * $response = $spannerClient->batchCreateSessions($formattedDatabase, $sessionCount); - * } finally { - * $spannerClient->close(); - * } - * ``` - * - * @param string $database Required. The database in which the new sessions are created. - * @param int $sessionCount Required. The number of sessions to be created in this batch call. - * The API can return fewer than the requested number of sessions. If a - * specific number of sessions are desired, the client can make additional - * calls to `BatchCreateSessions` (adjusting - * [session_count][google.spanner.v1.BatchCreateSessionsRequest.session_count] - * as necessary). - * @param array $optionalArgs { - * Optional. - * - * @type Session $sessionTemplate - * Parameters to apply to each created session. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\BatchCreateSessionsResponse - * - * @throws ApiException if the remote call fails - */ - public function batchCreateSessions( - $database, - $sessionCount, - array $optionalArgs = [] - ) { - $request = new BatchCreateSessionsRequest(); - $requestParamHeaders = []; - $request->setDatabase($database); - $request->setSessionCount($sessionCount); - $requestParamHeaders['database'] = $database; - if (isset($optionalArgs['sessionTemplate'])) { - $request->setSessionTemplate($optionalArgs['sessionTemplate']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'BatchCreateSessions', - BatchCreateSessionsResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Batches the supplied mutation groups in a collection of efficient - * transactions. All mutations in a group are committed atomically. However, - * mutations across groups can be committed non-atomically in an unspecified - * order and thus, they must be independent of each other. Partial failure is - * possible, that is, some groups might have been committed successfully, - * while some might have failed. The results of individual batches are - * streamed into the response as the batches are applied. - * - * `BatchWrite` requests are not replay protected, meaning that each mutation - * group can be applied more than once. Replays of non-idempotent mutations - * can have undesirable effects. For example, replays of an insert mutation - * can produce an already exists error or if you use generated or commit - * timestamp-based keys, it can result in additional rows being added to the - * mutation's table. We recommend structuring your mutation groups to be - * idempotent to avoid this issue. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - * $mutationGroups = []; - * // Read all responses until the stream is complete - * $stream = $spannerClient->batchWrite($formattedSession, $mutationGroups); - * foreach ($stream->readAll() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $spannerClient->close(); - * } - * ``` - * - * @param string $session Required. The session in which the batch request is to be run. - * @param MutationGroup[] $mutationGroups Required. The groups of mutations to be applied. - * @param array $optionalArgs { - * Optional. - * - * @type RequestOptions $requestOptions - * Common options for this request. - * @type bool $excludeTxnFromChangeStreams - * Optional. If you don't set the `exclude_txn_from_change_streams` option or - * if it's set to `false`, then any change streams monitoring columns modified - * by transactions will capture the updates made within that transaction. - * @type int $timeoutMillis - * Timeout to use for this call. - * } - * - * @return \Google\ApiCore\ServerStream - * - * @throws ApiException if the remote call fails - */ - public function batchWrite( - $session, - $mutationGroups, - array $optionalArgs = [] - ) { - $request = new BatchWriteRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setMutationGroups($mutationGroups); - $requestParamHeaders['session'] = $session; - if (isset($optionalArgs['requestOptions'])) { - $request->setRequestOptions($optionalArgs['requestOptions']); - } - - if (isset($optionalArgs['excludeTxnFromChangeStreams'])) { - $request->setExcludeTxnFromChangeStreams( - $optionalArgs['excludeTxnFromChangeStreams'] - ); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'BatchWrite', - BatchWriteResponse::class, - $optionalArgs, - $request, - Call::SERVER_STREAMING_CALL - ); - } - - /** - * 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: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[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 RequestOptions $requestOptions - * Common options for this request. - * Priority is ignored for this request. Setting the priority in this - * `request_options` struct doesn't do anything. To set the priority for a - * transaction, set it on the reads and writes that are part of this - * transaction instead. - * @type Mutation $mutationKey - * Optional. Required for read-write transactions on a multiplexed session - * that commit mutations but don't perform any reads or queries. You must - * randomly select one of the mutations from the mutation set and send it as a - * part of this request. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\Transaction - * - * @throws ApiException if the remote call fails - */ - public function beginTransaction( - $session, - $options, - array $optionalArgs = [] - ) { - $request = new BeginTransactionRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setOptions($options); - $requestParamHeaders['session'] = $session; - if (isset($optionalArgs['requestOptions'])) { - $request->setRequestOptions($optionalArgs['requestOptions']); - } - - if (isset($optionalArgs['mutationKey'])) { - $request->setMutationKey($optionalArgs['mutationKey']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'BeginTransaction', - Transaction::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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 retry - * the transaction from the beginning, reusing the same session. - * - * On very rare occasions, `Commit` might return `UNKNOWN`. This can happen, - * for example, if the client job experiences a 1+ hour networking failure. - * At that point, Cloud Spanner has lost track of the transaction outcome and - * we recommend that you perform another read from the database to see the - * state of things as they are now. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[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's 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 bool $returnCommitStats - * If `true`, then statistics related to the transaction is included in - * the [CommitResponse][google.spanner.v1.CommitResponse.commit_stats]. - * Default value is `false`. - * @type Duration $maxCommitDelay - * Optional. The amount of latency this request is configured to incur in - * order to improve throughput. If this field isn't set, Spanner assumes - * requests are relatively latency sensitive and automatically determines an - * appropriate delay time. You can specify a commit delay value between 0 and - * 500 ms. - * @type RequestOptions $requestOptions - * Common options for this request. - * @type MultiplexedSessionPrecommitToken $precommitToken - * Optional. If the read-write transaction was executed on a multiplexed - * session, then you must include the precommit token with the highest - * sequence number received in this transaction attempt. Failing to do so - * results in a `FailedPrecondition` error. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\CommitResponse - * - * @throws ApiException if the remote call fails - */ - public function commit($session, $mutations, array $optionalArgs = []) - { - $request = new CommitRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setMutations($mutations); - $requestParamHeaders['session'] = $session; - if (isset($optionalArgs['transactionId'])) { - $request->setTransactionId($optionalArgs['transactionId']); - } - - if (isset($optionalArgs['singleUseTransaction'])) { - $request->setSingleUseTransaction( - $optionalArgs['singleUseTransaction'] - ); - } - - if (isset($optionalArgs['returnCommitStats'])) { - $request->setReturnCommitStats($optionalArgs['returnCommitStats']); - } - - if (isset($optionalArgs['maxCommitDelay'])) { - $request->setMaxCommitDelay($optionalArgs['maxCommitDelay']); - } - - if (isset($optionalArgs['requestOptions'])) { - $request->setRequestOptions($optionalArgs['requestOptions']); - } - - if (isset($optionalArgs['precommitToken'])) { - $request->setPrecommitToken($optionalArgs['precommitToken']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'Commit', - CommitResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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. - * - * Active sessions use additional server resources, so it's a good idea to - * delete idle and unneeded sessions. - * Aside from explicit deletes, Cloud Spanner can delete sessions when 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, for example, `"SELECT 1"`. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedDatabase = $spannerClient->databaseName('[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 Session $session - * Required. The session to create. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\Session - * - * @throws ApiException if the remote call fails - */ - public function createSession($database, array $optionalArgs = []) - { - $request = new CreateSessionRequest(); - $requestParamHeaders = []; - $request->setDatabase($database); - $requestParamHeaders['database'] = $database; - if (isset($optionalArgs['session'])) { - $request->setSession($optionalArgs['session']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'CreateSession', - Session::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Ends a session, releasing server resources associated with it. This - * asynchronously triggers the cancellation of any operations that are running - * with this session. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedName = $spannerClient->sessionName('[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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @throws ApiException if the remote call fails - */ - public function deleteSession($name, array $optionalArgs = []) - { - $request = new DeleteSessionRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'DeleteSession', - GPBEmpty::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Executes a batch of SQL DML statements. This method allows many statements - * to be run with lower latency than submitting them sequentially with - * [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql]. - * - * Statements are executed in sequential order. A request can succeed even if - * a statement fails. The - * [ExecuteBatchDmlResponse.status][google.spanner.v1.ExecuteBatchDmlResponse.status] - * field in the response provides information about the statement that failed. - * Clients must inspect this field to determine whether an error occurred. - * - * Execution stops after the first failed statement; the remaining statements - * are not executed. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - * $transaction = new TransactionSelector(); - * $statements = []; - * $seqno = 0; - * $response = $spannerClient->executeBatchDml($formattedSession, $transaction, $statements, $seqno); - * } finally { - * $spannerClient->close(); - * } - * ``` - * - * @param string $session Required. The session in which the DML statements should be performed. - * @param TransactionSelector $transaction Required. The transaction to use. Must be a read-write transaction. - * - * To protect against replays, single-use transactions are not supported. The - * caller must either supply an existing transaction ID or begin a new - * transaction. - * @param Statement[] $statements Required. The list of statements to execute in this batch. Statements are - * executed serially, such that the effects of statement `i` are visible to - * statement `i+1`. Each statement must be a DML statement. Execution stops at - * the first failed statement; the remaining statements are not executed. - * - * Callers must provide at least one statement. - * @param int $seqno Required. A per-transaction sequence number used to identify this request. - * This field makes each request idempotent such that if the request is - * received multiple times, at most one succeeds. - * - * The sequence number must be monotonically increasing within the - * transaction. If a request arrives for the first time with an out-of-order - * sequence number, the transaction might be aborted. Replays of previously - * handled requests yield the same response as the first execution. - * @param array $optionalArgs { - * Optional. - * - * @type RequestOptions $requestOptions - * Common options for this request. - * @type bool $lastStatements - * Optional. If set to `true`, this request marks the end of the transaction. - * After these statements execute, you must commit or abort the transaction. - * Attempts to execute any other requests against this transaction - * (including reads and queries) are rejected. - * - * Setting this option might cause some error reporting to be deferred until - * commit time (for example, validation of unique constraints). Given this, - * successful execution of statements shouldn't be assumed until a subsequent - * `Commit` call completes successfully. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\ExecuteBatchDmlResponse - * - * @throws ApiException if the remote call fails - */ - public function executeBatchDml( - $session, - $transaction, - $statements, - $seqno, - array $optionalArgs = [] - ) { - $request = new ExecuteBatchDmlRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setTransaction($transaction); - $request->setStatements($statements); - $request->setSeqno($seqno); - $requestParamHeaders['session'] = $session; - if (isset($optionalArgs['requestOptions'])) { - $request->setRequestOptions($optionalArgs['requestOptions']); - } - - if (isset($optionalArgs['lastStatements'])) { - $request->setLastStatements($optionalArgs['lastStatements']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'ExecuteBatchDml', - ExecuteBatchDmlResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Executes an SQL statement, returning all results in a single reply. This - * method can't 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. - * - * Operations 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. - * - * The query string can be SQL or [Graph Query Language - * (GQL)](https://cloud.google.com/spanner/docs/reference/standard-sql/graph-intro). - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - * $sql = '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 string. - * @param array $optionalArgs { - * Optional. - * - * @type TransactionSelector $transaction - * The transaction to use. - * - * For queries, if none is provided, the default is a temporary read-only - * transaction with strong concurrency. - * - * Standard DML statements require a read-write transaction. To protect - * against replays, single-use transactions are not supported. The caller - * must either supply an existing transaction ID or begin a new transaction. - * - * Partitioned DML requires an existing Partitioned DML transaction ID. - * @type Struct $params - * Parameter names and values that bind to placeholders in the SQL string. - * - * A parameter placeholder consists of the `@` character followed by the - * parameter name (for example, `@firstName`). Parameter names must conform - * to the naming requirements of identifiers as specified at - * https://cloud.google.com/spanner/docs/lexical#identifiers. - * - * 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's an error to execute a SQL statement with unbound parameters. - * @type array $paramTypes - * It isn't 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, you can use `param_types` to specify the exact - * SQL type for some or all of the SQL statement 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 statement - * execution, `resume_token` should be copied from the last - * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the - * interruption. Doing this enables the new SQL statement 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 int $queryMode - * Used to control the amount of debugging information returned in - * [ResultSetStats][google.spanner.v1.ResultSetStats]. If - * [partition_token][google.spanner.v1.ExecuteSqlRequest.partition_token] is - * set, [query_mode][google.spanner.v1.ExecuteSqlRequest.query_mode] can only - * be set to - * [QueryMode.NORMAL][google.spanner.v1.ExecuteSqlRequest.QueryMode.NORMAL]. - * For allowed values, use constants defined on {@see \Google\Cloud\Spanner\V1\ExecuteSqlRequest\QueryMode} - * @type string $partitionToken - * If present, results are restricted to the specified partition - * previously created using `PartitionQuery`. There must be an exact - * match for the values of fields common to this message and the - * `PartitionQueryRequest` message used to create this `partition_token`. - * @type int $seqno - * A per-transaction sequence number used to identify this request. This field - * makes each request idempotent such that if the request is received multiple - * times, at most one succeeds. - * - * The sequence number must be monotonically increasing within the - * transaction. If a request arrives for the first time with an out-of-order - * sequence number, the transaction can be aborted. Replays of previously - * handled requests yield the same response as the first execution. - * - * Required for DML statements. Ignored for queries. - * @type QueryOptions $queryOptions - * Query optimizer configuration to use for the given query. - * @type RequestOptions $requestOptions - * Common options for this request. - * @type DirectedReadOptions $directedReadOptions - * Directed read options for this request. - * @type bool $dataBoostEnabled - * If this is for a partitioned query and this field is set to `true`, the - * request is executed with Spanner Data Boost independent compute resources. - * - * If the field is set to `true` but the request doesn't set - * `partition_token`, the API returns an `INVALID_ARGUMENT` error. - * @type bool $lastStatement - * Optional. If set to `true`, this statement marks the end of the - * transaction. After this statement executes, you must commit or abort the - * transaction. Attempts to execute any other requests against this - * transaction (including reads and queries) are rejected. - * - * For DML statements, setting this option might cause some error reporting to - * be deferred until commit time (for example, validation of unique - * constraints). Given this, successful execution of a DML statement shouldn't - * be assumed until a subsequent `Commit` call completes successfully. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\ResultSet - * - * @throws ApiException if the remote call fails - */ - public function executeSql($session, $sql, array $optionalArgs = []) - { - $request = new ExecuteSqlRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setSql($sql); - $requestParamHeaders['session'] = $session; - if (isset($optionalArgs['transaction'])) { - $request->setTransaction($optionalArgs['transaction']); - } - - if (isset($optionalArgs['params'])) { - $request->setParams($optionalArgs['params']); - } - - if (isset($optionalArgs['paramTypes'])) { - $request->setParamTypes($optionalArgs['paramTypes']); - } - - if (isset($optionalArgs['resumeToken'])) { - $request->setResumeToken($optionalArgs['resumeToken']); - } - - if (isset($optionalArgs['queryMode'])) { - $request->setQueryMode($optionalArgs['queryMode']); - } - - if (isset($optionalArgs['partitionToken'])) { - $request->setPartitionToken($optionalArgs['partitionToken']); - } - - if (isset($optionalArgs['seqno'])) { - $request->setSeqno($optionalArgs['seqno']); - } - - if (isset($optionalArgs['queryOptions'])) { - $request->setQueryOptions($optionalArgs['queryOptions']); - } - - if (isset($optionalArgs['requestOptions'])) { - $request->setRequestOptions($optionalArgs['requestOptions']); - } - - if (isset($optionalArgs['directedReadOptions'])) { - $request->setDirectedReadOptions( - $optionalArgs['directedReadOptions'] - ); - } - - if (isset($optionalArgs['dataBoostEnabled'])) { - $request->setDataBoostEnabled($optionalArgs['dataBoostEnabled']); - } - - if (isset($optionalArgs['lastStatement'])) { - $request->setLastStatement($optionalArgs['lastStatement']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'ExecuteSql', - ResultSet::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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. - * - * The query string can be SQL or [Graph Query Language - * (GQL)](https://cloud.google.com/spanner/docs/reference/standard-sql/graph-intro). - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - * $sql = '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 string. - * @param array $optionalArgs { - * Optional. - * - * @type TransactionSelector $transaction - * The transaction to use. - * - * For queries, if none is provided, the default is a temporary read-only - * transaction with strong concurrency. - * - * Standard DML statements require a read-write transaction. To protect - * against replays, single-use transactions are not supported. The caller - * must either supply an existing transaction ID or begin a new transaction. - * - * Partitioned DML requires an existing Partitioned DML transaction ID. - * @type Struct $params - * Parameter names and values that bind to placeholders in the SQL string. - * - * A parameter placeholder consists of the `@` character followed by the - * parameter name (for example, `@firstName`). Parameter names must conform - * to the naming requirements of identifiers as specified at - * https://cloud.google.com/spanner/docs/lexical#identifiers. - * - * 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's an error to execute a SQL statement with unbound parameters. - * @type array $paramTypes - * It isn't 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, you can use `param_types` to specify the exact - * SQL type for some or all of the SQL statement 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 statement - * execution, `resume_token` should be copied from the last - * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the - * interruption. Doing this enables the new SQL statement 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 int $queryMode - * Used to control the amount of debugging information returned in - * [ResultSetStats][google.spanner.v1.ResultSetStats]. If - * [partition_token][google.spanner.v1.ExecuteSqlRequest.partition_token] is - * set, [query_mode][google.spanner.v1.ExecuteSqlRequest.query_mode] can only - * be set to - * [QueryMode.NORMAL][google.spanner.v1.ExecuteSqlRequest.QueryMode.NORMAL]. - * For allowed values, use constants defined on {@see \Google\Cloud\Spanner\V1\ExecuteSqlRequest\QueryMode} - * @type string $partitionToken - * If present, results are restricted to the specified partition - * previously created using `PartitionQuery`. There must be an exact - * match for the values of fields common to this message and the - * `PartitionQueryRequest` message used to create this `partition_token`. - * @type int $seqno - * A per-transaction sequence number used to identify this request. This field - * makes each request idempotent such that if the request is received multiple - * times, at most one succeeds. - * - * The sequence number must be monotonically increasing within the - * transaction. If a request arrives for the first time with an out-of-order - * sequence number, the transaction can be aborted. Replays of previously - * handled requests yield the same response as the first execution. - * - * Required for DML statements. Ignored for queries. - * @type QueryOptions $queryOptions - * Query optimizer configuration to use for the given query. - * @type RequestOptions $requestOptions - * Common options for this request. - * @type DirectedReadOptions $directedReadOptions - * Directed read options for this request. - * @type bool $dataBoostEnabled - * If this is for a partitioned query and this field is set to `true`, the - * request is executed with Spanner Data Boost independent compute resources. - * - * If the field is set to `true` but the request doesn't set - * `partition_token`, the API returns an `INVALID_ARGUMENT` error. - * @type bool $lastStatement - * Optional. If set to `true`, this statement marks the end of the - * transaction. After this statement executes, you must commit or abort the - * transaction. Attempts to execute any other requests against this - * transaction (including reads and queries) are rejected. - * - * For DML statements, setting this option might cause some error reporting to - * be deferred until commit time (for example, validation of unique - * constraints). Given this, successful execution of a DML statement shouldn't - * be assumed until a subsequent `Commit` call completes successfully. - * @type int $timeoutMillis - * Timeout to use for this call. - * } - * - * @return \Google\ApiCore\ServerStream - * - * @throws ApiException if the remote call fails - */ - public function executeStreamingSql( - $session, - $sql, - array $optionalArgs = [] - ) { - $request = new ExecuteSqlRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setSql($sql); - $requestParamHeaders['session'] = $session; - if (isset($optionalArgs['transaction'])) { - $request->setTransaction($optionalArgs['transaction']); - } - - if (isset($optionalArgs['params'])) { - $request->setParams($optionalArgs['params']); - } - - if (isset($optionalArgs['paramTypes'])) { - $request->setParamTypes($optionalArgs['paramTypes']); - } - - if (isset($optionalArgs['resumeToken'])) { - $request->setResumeToken($optionalArgs['resumeToken']); - } - - if (isset($optionalArgs['queryMode'])) { - $request->setQueryMode($optionalArgs['queryMode']); - } - - if (isset($optionalArgs['partitionToken'])) { - $request->setPartitionToken($optionalArgs['partitionToken']); - } - - if (isset($optionalArgs['seqno'])) { - $request->setSeqno($optionalArgs['seqno']); - } - - if (isset($optionalArgs['queryOptions'])) { - $request->setQueryOptions($optionalArgs['queryOptions']); - } - - if (isset($optionalArgs['requestOptions'])) { - $request->setRequestOptions($optionalArgs['requestOptions']); - } - - if (isset($optionalArgs['directedReadOptions'])) { - $request->setDirectedReadOptions( - $optionalArgs['directedReadOptions'] - ); - } - - if (isset($optionalArgs['dataBoostEnabled'])) { - $request->setDataBoostEnabled($optionalArgs['dataBoostEnabled']); - } - - if (isset($optionalArgs['lastStatement'])) { - $request->setLastStatement($optionalArgs['lastStatement']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'ExecuteStreamingSql', - PartialResultSet::class, - $optionalArgs, - $request, - Call::SERVER_STREAMING_CALL - ); - } - - /** - * Gets a session. Returns `NOT_FOUND` if the session doesn't exist. - * This is mainly useful for determining whether a session is still - * alive. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedName = $spannerClient->sessionName('[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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\Session - * - * @throws ApiException if the remote call fails - */ - public function getSession($name, array $optionalArgs = []) - { - $request = new GetSessionRequest(); - $requestParamHeaders = []; - $request->setName($name); - $requestParamHeaders['name'] = $name; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'GetSession', - Session::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Lists all sessions in a given database. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedDatabase = $spannerClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - * // Iterate over pages of elements - * $pagedResponse = $spannerClient->listSessions($formattedDatabase); - * foreach ($pagedResponse->iteratePages() as $page) { - * foreach ($page as $element) { - * // doSomethingWith($element); - * } - * } - * // Alternatively: - * // Iterate through all elements - * $pagedResponse = $spannerClient->listSessions($formattedDatabase); - * foreach ($pagedResponse->iterateAllElements() as $element) { - * // doSomethingWith($element); - * } - * } finally { - * $spannerClient->close(); - * } - * ``` - * - * @param string $database Required. The database in which to list sessions. - * @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: - * - * * `labels.key` where key is the name of a label - * - * Some examples of using filters are: - * - * * `labels.env:*` --> The session has the label "env". - * * `labels.env:dev` --> The session has the label "env" and the value of - * the label contains the string "dev". - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\ApiCore\PagedListResponse - * - * @throws ApiException if the remote call fails - */ - public function listSessions($database, array $optionalArgs = []) - { - $request = new ListSessionsRequest(); - $requestParamHeaders = []; - $request->setDatabase($database); - $requestParamHeaders['database'] = $database; - if (isset($optionalArgs['pageSize'])) { - $request->setPageSize($optionalArgs['pageSize']); - } - - if (isset($optionalArgs['pageToken'])) { - $request->setPageToken($optionalArgs['pageToken']); - } - - if (isset($optionalArgs['filter'])) { - $request->setFilter($optionalArgs['filter']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->getPagedListResponse( - 'ListSessions', - $optionalArgs, - ListSessionsResponse::class, - $request - ); - } - - /** - * Creates a set of partition tokens that can be used to execute a query - * operation in parallel. Each of the returned partition tokens can be used - * by [ExecuteStreamingSql][google.spanner.v1.Spanner.ExecuteStreamingSql] to - * specify a subset of the query result to read. The same session and - * read-only transaction must be used by the `PartitionQueryRequest` used to - * create the partition tokens and the `ExecuteSqlRequests` that use the - * partition tokens. - * - * Partition tokens become invalid when the session used to create them - * is deleted, is idle for too long, begins a new transaction, or becomes too - * old. When any of these happen, it isn't possible to resume the query, and - * the whole operation must be restarted from the beginning. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - * $sql = 'sql'; - * $response = $spannerClient->partitionQuery($formattedSession, $sql); - * } finally { - * $spannerClient->close(); - * } - * ``` - * - * @param string $session Required. The session used to create the partitions. - * @param string $sql Required. The query request to generate partitions for. The request fails - * if the query isn't root partitionable. For a query to be root - * partitionable, it needs to satisfy a few conditions. For example, if the - * query execution plan contains a distributed union operator, then it must be - * the first operator in the plan. For more information about other - * conditions, see [Read data in - * parallel](https://cloud.google.com/spanner/docs/reads#read_data_in_parallel). - * - * The query request must not contain DML commands, such as `INSERT`, - * `UPDATE`, or `DELETE`. Use - * [`ExecuteStreamingSql`][google.spanner.v1.Spanner.ExecuteStreamingSql] with - * a `PartitionedDml` transaction for large, partition-friendly DML - * operations. - * @param array $optionalArgs { - * Optional. - * - * @type TransactionSelector $transaction - * Read-only snapshot transactions are supported, read and write and - * single-use transactions are not. - * @type Struct $params - * Parameter names and values that bind to placeholders in the SQL string. - * - * A parameter placeholder consists of the `@` character followed by the - * parameter name (for example, `@firstName`). Parameter names can contain - * 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's an error to execute a SQL statement with unbound parameters. - * @type array $paramTypes - * It isn't 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.PartitionQueryRequest.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 PartitionOptions $partitionOptions - * Additional options that affect how many partitions are created. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\PartitionResponse - * - * @throws ApiException if the remote call fails - */ - public function partitionQuery($session, $sql, array $optionalArgs = []) - { - $request = new PartitionQueryRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setSql($sql); - $requestParamHeaders['session'] = $session; - if (isset($optionalArgs['transaction'])) { - $request->setTransaction($optionalArgs['transaction']); - } - - if (isset($optionalArgs['params'])) { - $request->setParams($optionalArgs['params']); - } - - if (isset($optionalArgs['paramTypes'])) { - $request->setParamTypes($optionalArgs['paramTypes']); - } - - if (isset($optionalArgs['partitionOptions'])) { - $request->setPartitionOptions($optionalArgs['partitionOptions']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'PartitionQuery', - PartitionResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Creates a set of partition tokens that can be used to execute a read - * operation in parallel. Each of the returned partition tokens can be used - * by [StreamingRead][google.spanner.v1.Spanner.StreamingRead] to specify a - * subset of the read result to read. The same session and read-only - * transaction must be used by the `PartitionReadRequest` used to create the - * partition tokens and the `ReadRequests` that use the partition tokens. - * There are no ordering guarantees on rows returned among the returned - * partition tokens, or even within each individual `StreamingRead` call - * issued with a `partition_token`. - * - * Partition tokens become invalid when the session used to create them - * is deleted, is idle for too long, begins a new transaction, or becomes too - * old. When any of these happen, it isn't possible to resume the read, and - * the whole operation must be restarted from the beginning. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - * $table = 'table'; - * $keySet = new KeySet(); - * $response = $spannerClient->partitionRead($formattedSession, $table, $keySet); - * } finally { - * $spannerClient->close(); - * } - * ``` - * - * @param string $session Required. The session used to create the partitions. - * @param string $table Required. The name of the table in the database to be read. - * @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.PartitionReadRequest.table] to be yielded, unless - * [index][google.spanner.v1.PartitionReadRequest.index] is present. If - * [index][google.spanner.v1.PartitionReadRequest.index] is present, then - * [key_set][google.spanner.v1.PartitionReadRequest.key_set] instead names - * index keys in [index][google.spanner.v1.PartitionReadRequest.index]. - * - * It isn't an error for the `key_set` to name rows that don't - * exist in the database. Read yields nothing for nonexistent rows. - * @param array $optionalArgs { - * Optional. - * - * @type TransactionSelector $transaction - * Read only snapshot transactions are supported, read/write and single use - * transactions are not. - * @type string $index - * If non-empty, the name of an index on - * [table][google.spanner.v1.PartitionReadRequest.table]. This index is used - * instead of the table primary key when interpreting - * [key_set][google.spanner.v1.PartitionReadRequest.key_set] and sorting - * result rows. See [key_set][google.spanner.v1.PartitionReadRequest.key_set] - * for further information. - * @type string[] $columns - * The columns of [table][google.spanner.v1.PartitionReadRequest.table] to be - * returned for each row matching this request. - * @type PartitionOptions $partitionOptions - * Additional options that affect how many partitions are created. - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\PartitionResponse - * - * @throws ApiException if the remote call fails - */ - public function partitionRead( - $session, - $table, - $keySet, - array $optionalArgs = [] - ) { - $request = new PartitionReadRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setTable($table); - $request->setKeySet($keySet); - $requestParamHeaders['session'] = $session; - if (isset($optionalArgs['transaction'])) { - $request->setTransaction($optionalArgs['transaction']); - } - - if (isset($optionalArgs['index'])) { - $request->setIndex($optionalArgs['index']); - } - - if (isset($optionalArgs['columns'])) { - $request->setColumns($optionalArgs['columns']); - } - - if (isset($optionalArgs['partitionOptions'])) { - $request->setPartitionOptions($optionalArgs['partitionOptions']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'PartitionRead', - PartitionResponse::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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 can't 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: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - * $table = '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 Required. 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]. - * - * If the [partition_token][google.spanner.v1.ReadRequest.partition_token] - * field is empty, 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). If the - * [partition_token][google.spanner.v1.ReadRequest.partition_token] field - * isn't empty, rows are yielded in an unspecified order. - * - * It isn't an error for the `key_set` to name rows that don't - * 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. A limit can't be specified if - * `partition_token` is set. - * @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 string $partitionToken - * If present, results are restricted to the specified partition - * previously created using `PartitionRead`. There must be an exact - * match for the values of fields common to this message and the - * PartitionReadRequest message used to create this partition_token. - * @type RequestOptions $requestOptions - * Common options for this request. - * @type DirectedReadOptions $directedReadOptions - * Directed read options for this request. - * @type bool $dataBoostEnabled - * If this is for a partitioned read and this field is set to `true`, the - * request is executed with Spanner Data Boost independent compute resources. - * - * If the field is set to `true` but the request doesn't set - * `partition_token`, the API returns an `INVALID_ARGUMENT` error. - * @type int $orderBy - * Optional. Order for the returned rows. - * - * By default, Spanner returns result rows in primary key order except for - * PartitionRead requests. For applications that don't require rows to be - * returned in primary key (`ORDER_BY_PRIMARY_KEY`) order, setting - * `ORDER_BY_NO_ORDER` option allows Spanner to optimize row retrieval, - * resulting in lower latencies in certain cases (for example, bulk point - * lookups). - * For allowed values, use constants defined on {@see \Google\Cloud\Spanner\V1\ReadRequest\OrderBy} - * @type int $lockHint - * Optional. Lock Hint for the request, it can only be used with read-write - * transactions. - * For allowed values, use constants defined on {@see \Google\Cloud\Spanner\V1\ReadRequest\LockHint} - * @type RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @return \Google\Cloud\Spanner\V1\ResultSet - * - * @throws ApiException if the remote call fails - */ - public function read( - $session, - $table, - $columns, - $keySet, - array $optionalArgs = [] - ) { - $request = new ReadRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setTable($table); - $request->setColumns($columns); - $request->setKeySet($keySet); - $requestParamHeaders['session'] = $session; - 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']); - } - - if (isset($optionalArgs['partitionToken'])) { - $request->setPartitionToken($optionalArgs['partitionToken']); - } - - if (isset($optionalArgs['requestOptions'])) { - $request->setRequestOptions($optionalArgs['requestOptions']); - } - - if (isset($optionalArgs['directedReadOptions'])) { - $request->setDirectedReadOptions( - $optionalArgs['directedReadOptions'] - ); - } - - if (isset($optionalArgs['dataBoostEnabled'])) { - $request->setDataBoostEnabled($optionalArgs['dataBoostEnabled']); - } - - if (isset($optionalArgs['orderBy'])) { - $request->setOrderBy($optionalArgs['orderBy']); - } - - if (isset($optionalArgs['lockHint'])) { - $request->setLockHint($optionalArgs['lockHint']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'Read', - ResultSet::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * Rolls back a transaction, releasing any locks it holds. It's 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 isn't - * found. `Rollback` never returns `ABORTED`. - * - * Sample code: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[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 RetrySettings|array $retrySettings - * Retry settings to use for this call. Can be a {@see RetrySettings} object, or an - * associative array of retry settings parameters. See the documentation on - * {@see RetrySettings} for example usage. - * } - * - * @throws ApiException if the remote call fails - */ - public function rollback($session, $transactionId, array $optionalArgs = []) - { - $request = new RollbackRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setTransactionId($transactionId); - $requestParamHeaders['session'] = $session; - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'Rollback', - GPBEmpty::class, - $optionalArgs, - $request - )->wait(); - } - - /** - * 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: - * ``` - * $spannerClient = new SpannerClient(); - * try { - * $formattedSession = $spannerClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - * $table = '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 Required. 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]. - * - * If the [partition_token][google.spanner.v1.ReadRequest.partition_token] - * field is empty, 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). If the - * [partition_token][google.spanner.v1.ReadRequest.partition_token] field - * isn't empty, rows are yielded in an unspecified order. - * - * It isn't an error for the `key_set` to name rows that don't - * 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. A limit can't be specified if - * `partition_token` is set. - * @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 string $partitionToken - * If present, results are restricted to the specified partition - * previously created using `PartitionRead`. There must be an exact - * match for the values of fields common to this message and the - * PartitionReadRequest message used to create this partition_token. - * @type RequestOptions $requestOptions - * Common options for this request. - * @type DirectedReadOptions $directedReadOptions - * Directed read options for this request. - * @type bool $dataBoostEnabled - * If this is for a partitioned read and this field is set to `true`, the - * request is executed with Spanner Data Boost independent compute resources. - * - * If the field is set to `true` but the request doesn't set - * `partition_token`, the API returns an `INVALID_ARGUMENT` error. - * @type int $orderBy - * Optional. Order for the returned rows. - * - * By default, Spanner returns result rows in primary key order except for - * PartitionRead requests. For applications that don't require rows to be - * returned in primary key (`ORDER_BY_PRIMARY_KEY`) order, setting - * `ORDER_BY_NO_ORDER` option allows Spanner to optimize row retrieval, - * resulting in lower latencies in certain cases (for example, bulk point - * lookups). - * For allowed values, use constants defined on {@see \Google\Cloud\Spanner\V1\ReadRequest\OrderBy} - * @type int $lockHint - * Optional. Lock Hint for the request, it can only be used with read-write - * transactions. - * For allowed values, use constants defined on {@see \Google\Cloud\Spanner\V1\ReadRequest\LockHint} - * @type int $timeoutMillis - * Timeout to use for this call. - * } - * - * @return \Google\ApiCore\ServerStream - * - * @throws ApiException if the remote call fails - */ - public function streamingRead( - $session, - $table, - $columns, - $keySet, - array $optionalArgs = [] - ) { - $request = new ReadRequest(); - $requestParamHeaders = []; - $request->setSession($session); - $request->setTable($table); - $request->setColumns($columns); - $request->setKeySet($keySet); - $requestParamHeaders['session'] = $session; - 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']); - } - - if (isset($optionalArgs['partitionToken'])) { - $request->setPartitionToken($optionalArgs['partitionToken']); - } - - if (isset($optionalArgs['requestOptions'])) { - $request->setRequestOptions($optionalArgs['requestOptions']); - } - - if (isset($optionalArgs['directedReadOptions'])) { - $request->setDirectedReadOptions( - $optionalArgs['directedReadOptions'] - ); - } - - if (isset($optionalArgs['dataBoostEnabled'])) { - $request->setDataBoostEnabled($optionalArgs['dataBoostEnabled']); - } - - if (isset($optionalArgs['orderBy'])) { - $request->setOrderBy($optionalArgs['orderBy']); - } - - if (isset($optionalArgs['lockHint'])) { - $request->setLockHint($optionalArgs['lockHint']); - } - - $requestParams = new RequestParamsHeaderDescriptor( - $requestParamHeaders - ); - $optionalArgs['headers'] = isset($optionalArgs['headers']) - ? array_merge($requestParams->getHeader(), $optionalArgs['headers']) - : $requestParams->getHeader(); - return $this->startCall( - 'StreamingRead', - PartialResultSet::class, - $optionalArgs, - $request, - Call::SERVER_STREAMING_CALL - ); - } -} diff --git a/Spanner/src/V1/SpannerClient.php b/Spanner/src/V1/SpannerClient.php deleted file mode 100644 index 220b1b5ec6a6..000000000000 --- a/Spanner/src/V1/SpannerClient.php +++ /dev/null @@ -1,52 +0,0 @@ - PgNumeric::class, self::TYPE_PG_JSONB => PgJsonb::class, self::TYPE_PG_OID => PgOid::class, @@ -96,7 +97,7 @@ class ValueMapper * * @var array */ - private static $typeCodes = [ + private static array $typeCodes = [ self::TYPE_PG_NUMERIC => self::TYPE_NUMERIC, self::TYPE_PG_JSONB => self::TYPE_JSON, self::TYPE_PG_OID => self::TYPE_INT64, @@ -108,7 +109,7 @@ class ValueMapper * * @var array */ - private static $typeAnnotations = [ + private static array $typeAnnotations = [ self::TYPE_PG_NUMERIC => TypeAnnotationCode::PG_NUMERIC, self::TYPE_PG_JSONB => TypeAnnotationCode::PG_JSONB, self::TYPE_PG_OID => TypeAnnotationCode::PG_OID, @@ -117,12 +118,12 @@ class ValueMapper /** * @var bool */ - private $returnInt64AsObject; + private bool $returnInt64AsObject; /** * @param bool $returnInt64AsObject */ - public function __construct($returnInt64AsObject) + public function __construct(bool $returnInt64AsObject) { $this->returnInt64AsObject = $returnInt64AsObject; } @@ -136,7 +137,7 @@ public function __construct($returnInt64AsObject) * @param array $types The types of values. * @return array An associative array containing params and paramTypes. */ - public function formatParamsForExecuteSql(array $parameters, array $types = []) + public function formatParamsForExecuteSql(array $parameters, array $types = []): array { $paramTypes = []; @@ -156,7 +157,7 @@ public function formatParamsForExecuteSql(array $parameters, array $types = []) $definition = null; if ($type) { - list($type, $definition) = $this->resolveTypeDefinition($type, $key); + list($type, $definition) = $this->resolveTypeDefinition($type); } $paramDefinition = $this->paramType($value, $type, $definition); @@ -178,7 +179,7 @@ public function formatParamsForExecuteSql(array $parameters, array $types = []) * **Defaults to** `false`. * @return array The encoded values */ - public function encodeValuesAsSimpleType(array $values, $allowMixedArrayType = false) + public function encodeValuesAsSimpleType(array $values, $allowMixedArrayType = false): array { $res = []; foreach ($values as $value) { @@ -203,9 +204,9 @@ public function encodeValuesAsSimpleType(array $values, $allowMixedArrayType = f * @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 + * @throws InvalidArgumentException */ - public function decodeValues(array $columns, array $row, $format) + public function decodeValues(array $columns, array $row, string $format): array { switch ($format) { case Result::RETURN_NAME_VALUE_PAIR: @@ -236,7 +237,7 @@ public function decodeValues(array $columns, array $row, $format) return $row; default: - throw new \InvalidArgumentException('Invalid format provided.'); + throw new InvalidArgumentException('Invalid format provided.'); } } @@ -250,11 +251,10 @@ public function decodeValues(array $columns, array $row, $format) * both the type and definition prior to calling `paramType()`. * * @param mixed $type The type code or definition. - * @param string $key the parameter key name. * @return array Containing the type value at position 0, and definition or * null at position 1. */ - private function resolveTypeDefinition($type, $key = null) + private function resolveTypeDefinition($type): array { $definition = null; if (is_array($type)) { @@ -263,7 +263,7 @@ private function resolveTypeDefinition($type, $key = null) 2 => null ]; - $definition = new ArrayType($type[1], $type[2]); + $definition = new ArrayType($type[1]); $type = Database::TYPE_ARRAY; } elseif ($type instanceof ArrayType) { $definition = $type; @@ -283,7 +283,7 @@ private function resolveTypeDefinition($type, $key = null) * @param array $type The value type * @return mixed */ - private function decodeValue($value, array $type) + private function decodeValue(mixed $value, array $type): mixed { if ($value === null || $value === '') { return $value; @@ -406,11 +406,11 @@ private function decodeValue($value, array $type) * in the format: [, ['code' => , 'typeAnnotation' => ]]. */ private function paramType( - $value, + mixed $value, $givenType = null, $definition = null, $allowMixedArrayType = false - ) { + ): array { $valueType = gettype($value); $typeAnnotation = null; @@ -471,9 +471,9 @@ private function paramType( case 'array': if ($givenType === Database::TYPE_STRUCT) { if (!($definition instanceof StructType)) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'Struct parameter types must be declared explicitly, and must ' . - 'be an instance of Google\Cloud\Spanner\StructType.' + 'be an instance of `' . StructType::class . '`.' ); } @@ -481,14 +481,24 @@ private function paramType( $value = (array) $value; } + if (!($value instanceof StructValue) && !is_array($value) && $value !== null) { + throw new InvalidArgumentException( + 'Struct value must be an array an instance of `' . StructValue::class . '` or null.' + ); + } + list($value, $type) = $this->structParam($value, $definition); } else { if (!($definition instanceof ArrayType)) { - throw new \InvalidArgumentException( - 'Array parameter types must be an instance of Google\Cloud\Spanner\ArrayType.' + throw new InvalidArgumentException( + 'Array parameter types must be an instance of `' . ArrayType::class . '`.' ); } + if (!is_array($value) && $value !== null) { + throw new InvalidArgumentException('Array value must be an array or null.'); + } + list($value, $type) = $this->arrayParam($value, $definition, $allowMixedArrayType); } @@ -500,7 +510,7 @@ private function paramType( break; default: - throw new \InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( 'Unrecognized value type %s. ' . 'Please ensure you are using the latest version of google/cloud or google/cloud-spanner.', get_class($value) @@ -518,14 +528,8 @@ private function paramType( * @param StructType $type The struct type. * @return array{0: array, 1: array} An array containing the value and type. */ - private function structParam($value, StructType $type) + private function structParam(StructValue|array|null $value, StructType $type): array { - if (!($value instanceof StructValue) && !is_array($value) && $value !== null) { - throw new \InvalidArgumentException( - 'Struct value must be an array an instance of `Google\Cloud\Spanner\StructValue` or null.' - ); - } - $typeFields = $type->fields(); // iterate through types and values separately to accurately align @@ -659,12 +663,8 @@ private function structParam($value, StructType $type) * elements of differing types. * @return array{0: array, 1: array} An array containing the value and type. */ - private function arrayParam($value, ArrayType $arrayObj, $allowMixedArrayType = false) + private function arrayParam(array|null $value, ArrayType $arrayObj, bool $allowMixedArrayType = false): array { - if (!is_array($value) && $value !== null) { - throw new \InvalidArgumentException('Array value must be an array or null.'); - } - // tracks the diff typeCode, typeAnnotation of all elements // inside the array $inferredTypes = []; @@ -712,7 +712,7 @@ private function arrayParam($value, ArrayType $arrayObj, $allowMixedArrayType = } if (!$allowMixedArrayType && count($uniqueTypes) > 1) { - throw new \InvalidArgumentException('Array values may not be of mixed type'); + throw new InvalidArgumentException('Array values may not be of mixed type'); } // get typeCode either from the array type or the first element's inferred type @@ -726,7 +726,7 @@ private function arrayParam($value, ArrayType $arrayObj, $allowMixedArrayType = : null; if ($this->arrayDataMismatch($value, $typeCode, $typeAnnotationCode, $inferredTypes)) { - throw new \InvalidArgumentException('Array data does not match given array parameter type.'); + throw new InvalidArgumentException('Array data does not match given array parameter type.'); } if (is_null($typeCode) && count($inferredTypes) > 0 && isset($inferredTypes[0]['code'])) { @@ -765,12 +765,12 @@ private function arrayParam($value, ArrayType $arrayObj, $allowMixedArrayType = * Handle query parameter mappings for various types of objects. * * @param mixed $value The parameter value. - * @return array{0: array, 1: array} An array containing the value and type. + * @return array{0: array, 1: string} An array containing the value and type. */ - private function objectParam($value) + private function objectParam(mixed $value): array { if ($value instanceof \stdClass) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'Values of type `\stdClass` are interpreted as structs and must define their types.' ); } @@ -817,7 +817,7 @@ private function objectParam($value) ]; } - throw new \InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( 'Unrecognized value type %s. ' . 'Please ensure you are using the latest version of google/cloud or google/cloud-spanner.', get_class($value) @@ -833,14 +833,19 @@ private function objectParam($value) * the structure of an array or struct type. * @param string $nestedDefinitionType [optional] Either `arrayElementType` * or `structType`. - * @return array{0: array, 1: array} + * @return array{ + * code?: int, + * arrayElementType?: array, + * structType?: array, + * typeAnnotation?: int + * } */ private function typeObject( - $type, - $typeAnnotation = null, + int|null $type, + int|null $typeAnnotation = null, array $nestedDefinition = [], - $nestedDefinitionType = null - ) { + string|null $nestedDefinitionType = null + ): array { return array_filter([ 'code' => $type, $nestedDefinitionType => $nestedDefinition, @@ -861,7 +866,7 @@ private function typeObject( * @return string|int * @codingStandardsIgnoreEnd */ - private function getColumnName($columns, $index) + private function getColumnName(array $columns, int $index): string|int { return (isset($columns[$index]['name']) && $columns[$index]['name']) ? $columns[$index]['name'] @@ -874,7 +879,7 @@ private function getColumnName($columns, $index) * @param string $type * @return bool */ - private static function isCustomType($type) + private static function isCustomType(string|null $type): bool { return array_key_exists($type, self::$typeToClassMap); } @@ -886,7 +891,7 @@ private static function isCustomType($type) * @param string $type * @return int */ - private static function getTypeCodeFromString($type) + private static function getTypeCodeFromString(string $type): int { return array_key_exists($type, self::$typeCodes) ? self::$typeCodes[$type] : null; } @@ -898,7 +903,7 @@ private static function getTypeCodeFromString($type) * @param string $type * @return int */ - private static function getTypeAnnotationFromString($type) + private static function getTypeAnnotationFromString(string $type): int { return array_key_exists($type, self::$typeAnnotations) ? self::$typeAnnotations[$type] : null; } @@ -908,22 +913,28 @@ private static function getTypeAnnotationFromString($type) * * @return bool */ - private function arrayDataMismatch($value, $arrayTypeCode, $arrayTypeAnnotation, $inferredTypes) - { - $mismatch = false; - + private function arrayDataMismatch( + $value, + $arrayTypeCode, + $arrayTypeAnnotation, + array $inferredTypes + ): bool { if (!empty($value)) { - if ($arrayTypeCode && isset($inferredTypes[0]['code']) && - $arrayTypeCode !== $inferredTypes[0]['code']) { - $mismatch = true; + if ($arrayTypeCode + && isset($inferredTypes[0]['code']) + && $arrayTypeCode !== $inferredTypes[0]['code'] + ) { + return true; } - if ($arrayTypeAnnotation && isset($inferredTypes[0]['typeAnnotation']) && - $arrayTypeAnnotation !== $inferredTypes[0]['typeAnnotation']) { - $mismatch = true; + if ($arrayTypeAnnotation + && isset($inferredTypes[0]['typeAnnotation']) + && $arrayTypeAnnotation !== $inferredTypes[0]['typeAnnotation'] + ) { + return true; } } - return $mismatch; + return false; } } diff --git a/Spanner/tests/OperationRefreshTrait.php b/Spanner/tests/OperationRefreshTrait.php deleted file mode 100644 index 8707f30fb379..000000000000 --- a/Spanner/tests/OperationRefreshTrait.php +++ /dev/null @@ -1,41 +0,0 @@ -___setProperty('operation', new Operation($connection, $returnInt64AsObject)); - return $stub; - } -} diff --git a/Spanner/tests/Perf/ycsb.php b/Spanner/tests/Perf/ycsb.php index 05749d5bddcd..8d561a067bb7 100644 --- a/Spanner/tests/Perf/ycsb.php +++ b/Spanner/tests/Perf/ycsb.php @@ -56,7 +56,7 @@ $parameters = Config::getParameters(); $report = Report::getReporter(); -$database = (new SpannerClient)->connect($parameters['instance'], $parameters['database']); +$database = (new SpannerClient())->connect($parameters['instance'], $parameters['database']); $totalWeight = 0.0; $weights = []; diff --git a/Spanner/tests/ResultGeneratorTrait.php b/Spanner/tests/ResultGeneratorTrait.php index 6d7ccfc74be8..2067a5dac663 100644 --- a/Spanner/tests/ResultGeneratorTrait.php +++ b/Spanner/tests/ResultGeneratorTrait.php @@ -17,91 +17,128 @@ namespace Google\Cloud\Spanner\Tests; +use Google\ApiCore\ServerStream; use Google\Cloud\Spanner\Database; +use Google\Cloud\Spanner\Tests\Unit\Fixtures; +use Google\Cloud\Spanner\V1\PartialResultSet; +use Google\Cloud\Spanner\V1\ResultSetMetadata; +use Google\Cloud\Spanner\V1\ResultSetStats; +use Google\Cloud\Spanner\V1\StructType; +use Google\Cloud\Spanner\V1\StructType\Field; +use Google\Cloud\Spanner\V1\Transaction; +use Google\Cloud\Spanner\V1\Type; +use Google\Protobuf\Value; /** * Provide a Spanner Read/Query result */ trait ResultGeneratorTrait { - /** - * Yield a ResultSet response. - * - * @param bool $withStats If true, statistics will be included. - * **Defaults to** `false`. - * @param string|null $transaction If set, the value will be included as the - * transaction ID. **Defaults to** `null`. - * @return \Generator - */ - private function resultGenerator($withStats = false, $transaction = null) - { - return $this->yieldRows([ + private function resultGeneratorStream( + ?array $chunks = null, + ?ResultSetStats $stats = null, + ?string $transactionId = null + ) { + $stream = $this->prophesize(ServerStream::class); + $chunks = $chunks ?: [ [ 'name' => 'ID', 'type' => Database::TYPE_INT64, 'value' => '10' ] - ], $withStats, $transaction); - } + ]; - /** - * Yield rows with user-specified data. - * - * @param array[] $rows A list of arrays containing `name`, `type` and `value` keys. - * @param bool $withStats If true, statistics will be included. - * **Defaults to** `false`. - * @param string|null $transaction If set, the value will be included as the - * transaction ID. **Defaults to** `null`. - * @return \Generator - */ - private function yieldRows(array $rows, $withStats = false, $transaction = null) - { - $fields = []; - $values = []; - foreach ($rows as $row) { - $fields[] = [ - 'name' => $row['name'], - 'type' => [ - 'code' => $row['type'] - ] - ]; + $rows = []; - $values[] = $row['value']; + if ($chunks) { + foreach ($chunks as $i => $chunk) { + if (is_string($chunk)) { + // merge from JSON string + $result = new PartialResultSet(); + $result->mergeFromJsonString($chunk); + $rows[$i] = $result; + } elseif ($chunk instanceof PartialResultSet) { + $rows[$i] = $chunk; + } + } } - $result = [ - 'metadata' => [ - 'rowType' => [ - 'fields' => $fields - ] - ], - 'values' => $values - ]; + if (!$rows) { + $fields = []; + $values = []; + foreach ($chunks as $row) { + $fields[] = new Field([ + 'name' => $row['name'], + 'type' => new Type(['code' => $row['type']]) + ]); + + $values[] = new Value(['string_value' => (string) $row['value']]); + } - if ($withStats) { - $result['stats'] = [ - 'rowCountExact' => 1, - 'rowCountLowerBound' => 1 + $result = [ + 'metadata' => new ResultSetMetadata([ + 'row_type' => new StructType([ + 'fields' => $fields + ]) + ]), + 'values' => $values ]; + + if ($stats) { + $result['stats'] = $stats; + } + + if ($transactionId) { + $result['metadata']->setTransaction(new Transaction(['id' => $transactionId])); + } + + if (isset($result['stats'])) { + $result['stats'] = $stats; + } + + $rows[] = new PartialResultSet($result); } - if ($transaction) { - $result['metadata']['transaction'] = [ - 'id' => $transaction - ]; + $stream->readAll() + ->willReturn($this->resultGeneratorArray($rows)); + + return $stream->reveal(); + } + + private function resultGeneratorArray($chunks) + { + foreach ($chunks as $chunk) { + yield $chunk; } + } - yield $result; + private function resultGeneratorJson($chunks) + { + foreach ($chunks as $chunk) { + yield json_decode($chunk, true); + } } - /** - * Yield the given array as a generator. - * - * @param array $data The input data - * @return \Generator - */ - private function resultGeneratorData(array $data) + private function getStreamingDataFixture() { - yield $data; + return json_decode( + file_get_contents(Fixtures::STREAMING_READ_ACCEPTANCE_FIXTURE()), + true + ); + } + + public function streamingDataProviderFirstChunk() + { + foreach ($this->getStreamingDataFixture()['tests'] as $test) { + yield [$test['chunks'], $test['result']['value']]; + break; + } + } + + public function streamingDataProvider() + { + foreach ($this->getStreamingDataFixture()['tests'] as $test) { + yield [$test['chunks'], $test['result']['value']]; + } } } diff --git a/Spanner/tests/Snippet/ArrayTypeTest.php b/Spanner/tests/Snippet/ArrayTypeTest.php index db0b98de2eac..0ba9ab675a13 100644 --- a/Spanner/tests/Snippet/ArrayTypeTest.php +++ b/Spanner/tests/Snippet/ArrayTypeTest.php @@ -17,19 +17,21 @@ namespace Google\Cloud\Spanner\Tests\Snippet; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\ArrayType; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\StructType; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\PartialResultSet; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -40,17 +42,17 @@ class ArrayTypeTest extends SnippetTestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; - use StubCreationTrait; + use ApiHelperTrait; + use ResultGeneratorTrait; const PROJECT = 'my-awesome-project'; const DATABASE = 'my-database'; const INSTANCE = 'my-instance'; - private $connection; private $database; - private $type; + private $spannerClient; + private $serializer; public function setUp(): void { @@ -67,8 +69,7 @@ public function setUp(): void ]); $session->name() ->willReturn('database'); - $session->setExpiration(Argument::any()) - ->willReturn(100); + $session->setExpiration(Argument::any()); $sessionPool = $this->prophesize(SessionPoolInterface::class); $sessionPool->acquire(Argument::any()) @@ -76,54 +77,74 @@ public function setUp(): void $sessionPool->setDatabase(Argument::any()) ->willReturn(null); - $this->connection = $this->getConnStub(); - $this->database = TestHelpers::stub(Database::class, [ - $this->connection->reveal(), + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->serializer = new Serializer(); + + $this->database = new Database( + $this->spannerClient->reveal(), + $this->prophesize(DatabaseAdminClient::class)->reveal(), + $this->serializer, $instance->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], self::PROJECT, self::DATABASE, - $sessionPool->reveal() - ], ['operation']); + ['sessionPool' => $sessionPool->reveal()], + ); } public function testConstructor() { - $field = [ - 'code' => Database::TYPE_ARRAY, - 'arrayElementType' => [ - 'code' => Database::TYPE_STRING - ] - ]; - - $values = [ - 'foo', 'bar', null - ]; - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', 'SELECT @arrayParam as arrayValue'), - Argument::withEntry('params', [ - 'arrayParam' => $values - ]), - Argument::withEntry('paramTypes', [ - 'arrayParam' => $field - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals('SELECT @arrayParam as arrayValue', $request->getSql()); + $this->assertEquals( + ['arrayParam' => ['foo', 'bar', null]], + $message['params'] + ); + $this->assertEquals( + Database::TYPE_STRING, + $message['paramTypes']['arrayParam']['arrayElementType']['code'], + ); + $this->assertEquals( + Database::TYPE_ARRAY, + $message['paramTypes']['arrayParam']['code'], + ); + return true; + }), + Argument::type('array') + )->shouldBeCalled()->willReturn( + $this->resultGeneratorStream([$this->serializer->decodeMessage( + new PartialResultSet(), + [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'arrayValue', + 'type' => [ + 'code' => Database::TYPE_ARRAY, + 'arrayElementType' => [ + 'code' => Database::TYPE_STRING + ] + ] + ] + ] + ] + ], + 'values' => [ [ - 'name' => 'arrayValue', - 'type' => $field + 'listValue' => [ + 'values' => [ + ['stringValue' => 'foo'], + ['stringValue' => 'bar'], + ['nullValue' => 0] + ] + ] ] ] ] - ], - 'values' => [$values] - ])); - - $this->refreshOperation($this->database, $this->connection->reveal()); + )->serializeToJsonString()]) + ); $snippet = $this->snippetFromClass(ArrayType::class); $snippet->replace('$database = $spanner->connect(\'my-instance\', \'my-database\');', ''); @@ -140,9 +161,4 @@ public function testArrayTypeStruct() $this->assertEquals(Database::TYPE_STRUCT, $res->type()); $this->assertInstanceOf(StructType::class, $res->structType()); } - - private function resultGenerator(array $data) - { - yield $data; - } } diff --git a/Spanner/tests/Snippet/BackupTest.php b/Spanner/tests/Snippet/BackupTest.php index ea4b68179451..13c38f82d8c2 100644 --- a/Spanner/tests/Snippet/BackupTest.php +++ b/Spanner/tests/Snippet/BackupTest.php @@ -17,21 +17,35 @@ namespace Google\Cloud\Spanner\Tests\Snippet; +use Google\ApiCore\OperationResponse; +use Google\ApiCore\Page; +use Google\ApiCore\PagedListResponse; use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Connection\ConnectionInterface; +use Google\Cloud\Spanner\Admin\Database\V1\Backup as BackupProto; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\CopyBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\CreateBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\DeleteBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\GetBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\UpdateBackupRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\Backup; +use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\SpannerClient; +use Google\LongRunning\Client\OperationsClient; +use Google\LongRunning\ListOperationsRequest; +use Google\LongRunning\ListOperationsResponse; +use Google\LongRunning\Operation; +use Google\Protobuf\Timestamp as TimestampProto; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; - /** +/** * @group spanner * @group spanner-backup */ @@ -45,9 +59,10 @@ class BackupTest extends SnippetTestCase const DATABASE = 'my-database'; const BACKUP = 'my-backup'; - private $connection; + private $serializer; + private $operationResponse; + private $databaseAdminClient; private $backup; - private $client; private $instance; private $expireTime; @@ -55,25 +70,26 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->prophesize(ConnectionInterface::class); - $this->client = TestHelpers::stub(SpannerClient::class); - $this->expireTime = new \DateTime("+ 7 hours"); - $this->instance = TestHelpers::stub(Instance::class, [ - $this->connection->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], - self::PROJECT, - self::INSTANCE - ], ['connection', 'lroConnection']); - - $this->backup = TestHelpers::stub(Backup::class, [ - $this->connection->reveal(), - $this->instance, - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], + $this->databaseAdminClient = $this->prophesize(DatabaseAdminClient::class); + + $this->serializer = new Serializer(); + + $this->operationResponse = $this->prophesize(OperationResponse::class); + + $this->expireTime = new \DateTime('+ 7 hours'); + $database = $this->prophesize(Database::class); + $database->name()->willReturn(DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE)); + $instance = $this->prophesize(Instance::class); + $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); + $instance->database('my-database')->willReturn($database->reveal()); + + $this->backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $instance->reveal(), self::PROJECT, - self::BACKUP, - ], ['instance', 'connection', 'lroConnection']); + self::BACKUP + ); } public function testClass() @@ -93,13 +109,12 @@ public function testCreate() $snippet = $this->snippetFromMethod(Backup::class, 'create'); $snippet->addLocal('backup', $this->backup); - $this->connection->createBackup(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->createBackup( + Argument::type(CreateBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); @@ -107,16 +122,28 @@ public function testCreate() public function testCreateCopy() { - $snippet = $this->snippetFromMethod(Backup::class, 'createCopy'); - $snippet->addLocal('spanner', $this->client); + $sourceInstance = $this->prophesize(Instance::class); + $destInstance = $this->prophesize(Instance::class); + $sourceInstance->backup('source-backup-id') + ->shouldBeCalledOnce() + ->willReturn($this->backup); + $destInstance->backup('new-backup-id') + ->shouldBeCalledOnce() + ->willReturn($this->backup); + + $spanner = $this->prophesize(SpannerClient::class); + $spanner->instance('source-instance-id')->willReturn($sourceInstance); + $spanner->instance('destination-instance-id')->willReturn($destInstance); - $this->connection->copyBackup(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); + $snippet = $this->snippetFromMethod(Backup::class, 'createCopy'); + $snippet->addLocal('spanner', $spanner->reveal()); - $this->client->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->copyBackup( + Argument::type(CopyBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); @@ -127,10 +154,11 @@ public function testDelete() $snippet = $this->snippetFromMethod(Backup::class, 'delete'); $snippet->addLocal('backup', $this->backup); - $this->connection->deleteBackup(Argument::any()) - ->shouldBeCalled(); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->deleteBackup( + Argument::type(DeleteBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce(); $snippet->invoke(); } @@ -140,11 +168,12 @@ public function testExists() $snippet = $this->snippetFromMethod(Backup::class, 'exists'); $snippet->addLocal('backup', $this->backup); - $this->connection->getBackup(Argument::any()) - ->shouldBeCalled() - ->willReturn(['foo' => 'bar']); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getBackup( + Argument::type(GetBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new BackupProto()); $res = $snippet->invoke(); $this->assertEquals('Backup exists!', $res->output()); @@ -157,14 +186,15 @@ public function testInfo() $snippet = $this->snippetFromMethod(Backup::class, 'info'); $snippet->addLocal('backup', $this->backup); - $this->connection->getBackup(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($backup); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getBackup( + Argument::type(GetBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new BackupProto($backup)); $res = $snippet->invoke('info'); - $this->assertEquals($backup, $res->returnVal()); + $this->assertEquals($backup['name'], $res->returnVal()['name']); $snippet->invoke(); } @@ -182,19 +212,20 @@ public function testName() public function testReload() { - $bkp = ['name' => 'foo']; + $backup = ['name' => 'foo']; $snippet = $this->snippetFromMethod(Backup::class, 'reload'); $snippet->addLocal('backup', $this->backup); - $this->connection->getBackup(Argument::any()) + $this->databaseAdminClient->getBackup( + Argument::type(GetBackupRequest::class), + Argument::type('array') + ) ->shouldBeCalledTimes(2) - ->willReturn($bkp); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + ->willReturn(new BackupProto($backup)); $res = $snippet->invoke('info'); - $this->assertEquals($bkp, $res->returnVal()); + $this->assertEquals($backup['name'], $res->returnVal()['name']); $snippet->invoke(); } @@ -203,11 +234,12 @@ public function testState() $snippet = $this->snippetFromMethod(Backup::class, 'state'); $snippet->addLocal('backup', $this->backup); - $this->connection->getBackup(Argument::any()) - ->shouldBeCalledTimes(1) - ->WillReturn(['state' => Backup::STATE_READY]); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getBackup( + Argument::type(GetBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new BackupProto(['state' => Backup::STATE_READY])); $res = $snippet->invoke(); $this->assertEquals('Backup is ready!', $res->output()); @@ -215,19 +247,27 @@ public function testState() public function testUpdateExpireTime() { - $bkp = ['name' => 'foo', 'expireTime' => $this->expireTime->format('Y-m-d\TH:i:s.u\Z')]; + $backup = [ + 'name' => 'foo', + 'expire_time' => new TimestampProto(['seconds' => $this->expireTime->format('U')]) + ]; $snippet = $this->snippetFromMethod(Backup::class, 'updateExpireTime'); $snippet->addLocal('backup', $this->backup); - $this->connection->updateBackup(Argument::any()) - ->shouldBeCalled() - ->willReturn($bkp); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->updateBackup( + Argument::type(UpdateBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new BackupProto($backup)); $res = $snippet->invoke('info'); - $this->assertEquals($bkp, $res->returnVal()); + $this->assertEquals($backup['name'], $res->returnVal()['name']); + $this->assertEquals( + $this->expireTime->format('Y-m-d\TH:i:s.000000\Z'), + $res->returnVal()['expireTime'] + ); } public function testResumeOperation() @@ -246,18 +286,27 @@ public function testLongRunningOperations() $snippet = $this->snippetFromMethod(Backup::class, 'longRunningOperations'); $snippet->addLocal('backup', $this->backup); - $lroConnection = $this->prophesize(LongRunningConnectionInterface::class); - $lroConnection->operations(Argument::any()) + $operation = new Operation(); + + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListOperationsResponse(['operations' => [$operation]])); + $page->getNextPageToken() + ->willReturn(null); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + + $operationsClient = $this->prophesize(OperationsClient::class); + $operationsClient->listOperations( + Argument::type(ListOperationsRequest::class), + Argument::type('array') + ) + ->willReturn($pagedListResponse->reveal()); + + $this->databaseAdminClient->getOperationsClient() ->shouldBeCalled() - ->willReturn([ - 'operations' => [ - [ - 'name' => 'foo' - ] - ] - ]); - - $this->backup->___setProperty('lroConnection', $lroConnection->reveal()); + ->willReturn($operationsClient->reveal()); $res = $snippet->invoke('operations'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); diff --git a/Spanner/tests/Snippet/Batch/BatchClientTest.php b/Spanner/tests/Snippet/Batch/BatchClientTest.php index f4a3e9319f2b..ef30783ad093 100644 --- a/Spanner/tests/Snippet/Batch/BatchClientTest.php +++ b/Spanner/tests/Snippet/Batch/BatchClientTest.php @@ -17,23 +17,33 @@ namespace Google\Cloud\Spanner\Tests\Snippet\Batch; -use Google\Cloud\Core\RequestHandler; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; +use Google\Cloud\PubSub\Message; use Google\Cloud\PubSub\PubSubClient; -use Google\Cloud\PubSub\V1\Client\PublisherClient; -use Google\Cloud\PubSub\V1\Client\SubscriberClient; +use Google\Cloud\PubSub\Subscription; +use Google\Cloud\PubSub\Topic; use Google\Cloud\Spanner\Batch\BatchClient; use Google\Cloud\Spanner\Batch\BatchSnapshot; use Google\Cloud\Spanner\Batch\QueryPartition; -use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Operation; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Serializer; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; use Google\Cloud\Spanner\Timestamp; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\DeleteSessionRequest; +use Google\Cloud\Spanner\V1\PartialResultSet; +use Google\Cloud\Spanner\V1\Partition; +use Google\Cloud\Spanner\V1\PartitionQueryRequest; +use Google\Cloud\Spanner\V1\PartitionResponse; +use Google\Cloud\Spanner\V1\Session as SessionProto; +use Google\Cloud\Spanner\V1\Transaction as TransactionProto; +use Google\Protobuf\Timestamp as TimestampProto; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; /** * @group spanner @@ -41,26 +51,28 @@ */ class BatchClientTest extends SnippetTestCase { + use ProphecyTrait; use GrpcTestTrait; - use OperationRefreshTrait; - use StubCreationTrait; + use ResultGeneratorTrait; const DATABASE = 'projects/my-awesome-project/instances/my-instance/databases/my-database'; const SESSION = 'projects/my-awesome-project/instances/my-instance/databases/my-database/sessions/session-id'; const TRANSACTION = 'transaction-id'; - private $connection; + private $spannerClient; + private $serializer; private $client; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); - $this->client = TestHelpers::stub(BatchClient::class, [ - new Operation($this->connection->reveal(), false), + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->serializer = new Serializer(); + $this->client = new BatchClient( + new Operation($this->spannerClient->reveal(), $this->serializer), self::DATABASE - ], ['operation']); + ); } public function testClass() @@ -100,105 +112,111 @@ public function testPubSubExample() $message2 = $message1; $message2['attributes']['partition'] = $partition2->serialize(); - if (!property_exists(PubSubClient::class, 'requestHandler')) { - $this->markTestSkipped("Skipping testPubSubExample test as property 'requestHandler' is missing"); - } - // setup pubsub service call stubs - $pubsub = TestHelpers::stub(PubSubClient::class, [['projectId' => 'test']], ['requestHandler']); - $requestHandler = $this->prophesize(RequestHandler::class); - $requestHandler->sendRequest( - PublisherClient::class, - 'publish', - Argument::cetera() - )->shouldBeCalled() - ->will(function () use ($requestHandler) { - $requestHandler->sendRequest( - PublisherClient::class, - 'publish', - Argument::cetera() - )->shouldBeCalled(); - }); - - $requestHandler->sendRequest( - SubscriberClient::class, - 'pull', - Argument::cetera() - )->shouldBeCalled() - ->willReturn([ - 'receivedMessages' => [ - [ - 'message' => [ - 'attributes' => [ - 'snapshot' => $snapshotString, - 'partition' => $partition1->serialize() - ] + $topic = $this->prophesize(Topic::class); + $topic->publish(Argument::cetera()) + ->shouldBeCalledTimes(2); + $pubsub = $this->prophesize(PubSubClient::class); + $pubsub->topic(Argument::cetera()) + ->shouldBeCalled() + ->willReturn($topic->reveal()); + + $subscription = $this->prophesize(Subscription::class); + $subscription->pull(Argument::cetera()) + ->shouldBeCalledOnce() + ->willReturn([ + new Message([ + 'attributes' => [ + 'snapshot' => $snapshotString, + 'partition' => $partition1->serialize() ] - ] - ] - ]); + ]) + ]); - $pubsub->___setProperty('requestHandler', $requestHandler->reveal()); + $pubsub->subscription(Argument::cetera()) + ->shouldBeCalledOnce() + ->willReturn($subscription->reveal()); // setup spanner service call stubs - $this->connection->partitionQuery(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $this->spannerClient->partitionQuery( + Argument::type(PartitionQueryRequest::class), + Argument::type('array') + )->willReturn(new PartitionResponse([ 'partitions' => [ - ['partitionToken' => $partition1->token()], - ['partitionToken' => $partition2->token()] + new Partition(['partition_token' => $partition1->token()]), + new Partition(['partition_token' => $partition2->token()]), ] - ]); - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('partitionToken', $partition1->token()), - Argument::withEntry('transaction', ['id' => self::TRANSACTION]), - Argument::withEntry('session', self::SESSION) - ))->shouldBeCalled()->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'loginCount', - 'type' => [ - 'code' => Database::TYPE_INT64 + ])); + + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) use ($partition1) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['partitionToken'], + $partition1->token() + ); + $this->assertEquals( + $message['transaction']['id'], + self::TRANSACTION + ); + $this->assertEquals($message['session'], self::SESSION); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([$this->serializer->decodeMessage( + new PartialResultSet(), + [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => Database::TYPE_INT64 + ] + ] ] ] + ], + 'values' => [ + ['numberValue' => 0] ] ] - ], - 'values' => [0] - ])); - - $this->connection->createSession(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => self::SESSION - ]); - - $this->connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + )])); + + $this->spannerClient->createSession( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + )->willReturn(new SessionProto(['name' => self::SESSION])); + + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto([ 'id' => self::TRANSACTION, - 'readTimestamp' => \DateTime::createFromFormat('U', (string) $time)->format(Timestamp::FORMAT) - ]); + 'read_timestamp' => new TimestampProto(['seconds' => $time]) + ])); - $this->connection->deleteSession(Argument::any()) - ->shouldBeCalled(); + $this->spannerClient->deleteSession( + Argument::type(DeleteSessionRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce(); // inject clients $publisher->addLocal('batch', $this->client); - $publisher->addLocal('pubsub', $pubsub); + $publisher->addLocal('pubsub', $pubsub->reveal()); $publisher->replace('$pubsub = new PubSubClient();', ''); $publisher->insertAfterLine(0, 'function areWorkersDone() { return true; }'); $subscriber->addLocal('batch', $this->client); - $subscriber->addLocal('pubsub', $pubsub); + $subscriber->addLocal('pubsub', $pubsub->reveal()); $subscriber->replace('$pubsub = new PubSubClient();', ''); $publisher->insertAfterLine(0, 'function processResult($res) {iterator_to_array($res);}'); - $this->refreshOperation($this->client, $this->connection->reveal()); $publisher->invoke(); - $subscriber->invoke(); } @@ -209,19 +227,18 @@ public function testSnapshot() $time = time(); - $this->connection->createSession(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn([ - 'name' => self::SESSION - ]); - $this->connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto([ 'id' => self::TRANSACTION, - 'readTimestamp' => \DateTime::createFromFormat('U', (string) $time)->format(Timestamp::FORMAT) - ]); - - $this->refreshOperation($this->client, $this->connection->reveal()); + 'read_timestamp' => new TimestampProto(['seconds' => $time]) + ])); + $this->spannerClient->createSession( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + )->willReturn(new SessionProto(['name' => self::SESSION])); $res = $snippet->invoke('snapshot'); $this->assertInstanceOf(BatchSnapshot::class, $res->returnVal()); @@ -229,7 +246,7 @@ public function testSnapshot() public function testSnapshotFromString() { - $timestamp = new Timestamp(new \DateTime); + $timestamp = new Timestamp(new \DateTime()); $identifier = base64_encode(json_encode([ 'sessionName' => self::SESSION, diff --git a/Spanner/tests/Snippet/Batch/BatchSnapshotTest.php b/Spanner/tests/Snippet/Batch/BatchSnapshotTest.php index d35ac96696a3..2a01a96cb114 100644 --- a/Spanner/tests/Snippet/Batch/BatchSnapshotTest.php +++ b/Spanner/tests/Snippet/Batch/BatchSnapshotTest.php @@ -19,7 +19,6 @@ use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Spanner\Batch\BatchClient; use Google\Cloud\Spanner\Batch\BatchSnapshot; use Google\Cloud\Spanner\Batch\PartitionInterface; @@ -27,11 +26,22 @@ use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; use Google\Cloud\Spanner\Timestamp; -use Google\Cloud\Spanner\V1\SpannerClient; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\PartialResultSet; +use Google\Cloud\Spanner\V1\Partition; +use Google\Cloud\Spanner\V1\PartitionQueryRequest; +use Google\Cloud\Spanner\V1\PartitionReadRequest; +use Google\Cloud\Spanner\V1\PartitionResponse; +use Google\Cloud\Spanner\V1\Session as SessionProto; +use Google\Cloud\Spanner\V1\Transaction; +use Google\Protobuf\Timestamp as TimestampProto; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -42,15 +52,15 @@ class BatchSnapshotTest extends SnippetTestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; - use StubCreationTrait; + use ResultGeneratorTrait; const DATABASE = 'projects/my-awesome-project/instances/my-instance/databases/my-database'; const SESSION = 'projects/my-awesome-project/instances/my-instance/databases/my-database/sessions/session-id'; const TRANSACTION = 'transaction-id'; - private $connection; + private $spannerClient; + private $serializer; private $session; private $time; private $snapshot; @@ -59,7 +69,8 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); + $this->serializer = new Serializer(); + $this->spannerClient = $this->prophesize(SpannerClient::class); $sessData = SpannerClient::parseName(self::SESSION, 'session'); $this->session = $this->prophesize(Session::class); @@ -70,34 +81,38 @@ public function setUp(): void ]); $this->time = time(); - $this->snapshot = TestHelpers::stub(BatchSnapshot::class, [ - new Operation($this->connection->reveal(), false), + $this->snapshot = new BatchSnapshot( + new Operation($this->spannerClient->reveal(), $this->serializer), $this->session->reveal(), [ 'id' => self::TRANSACTION, 'readTimestamp' => new Timestamp(\DateTime::createFromFormat('U', (string) $this->time)) ] - ], ['operation', 'session']); + ); } public function testClass() { - $this->connection->createSession(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn([ - 'name' => self::SESSION - ]); - $this->connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $this->spannerClient->createSession( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + )->willReturn(new SessionProto(['name' => self::SESSION])); + + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new Transaction([ 'id' => self::TRANSACTION, - 'readTimestamp' => \DateTime::createFromFormat('U', (string) $this->time)->format(Timestamp::FORMAT) - ]); + 'read_timestamp' => new TimestampProto([ + 'seconds' => $this->time + ]) + ])); - $client = TestHelpers::stub(BatchClient::class, [ - new Operation($this->connection->reveal(), false), + $client = new BatchClient( + new Operation($this->spannerClient->reveal(), $this->serializer), self::DATABASE - ]); + ); $snippet = $this->snippetFromClass(BatchSnapshot::class); $snippet->setLine(3, ''); @@ -121,7 +136,7 @@ public function testSerializeSnapshot($index) public function provideSerializeIndex() { - return [[1],[2]]; + return [[1], [2]]; } public function testClose() @@ -129,39 +144,50 @@ public function testClose() $this->session->delete([]) ->shouldBeCalled(); - $this->snapshot->___setProperty('session', $this->session->reveal()); - $snippet = $this->snippetFromMethod(BatchSnapshot::class, 'close'); $snippet->addLocal('snapshot', $this->snapshot); $res = $snippet->invoke(); } - /** - * @dataProvider providePartitionMethods - */ - public function testPartitionRead($method) + public function testPartitionRead() { - $this->connection->$method(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $this->spannerClient->partitionRead( + Argument::type(PartitionReadRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new PartitionResponse([ 'partitions' => [ - ['partitionToken' => 'foo'] + new Partition(['partition_token' => 'foo']) ] - ]); - - $this->refreshOperation($this->snapshot, $this->connection->reveal()); + ])); - $snippet = $this->snippetFromMethod(BatchSnapshot::class, $method); + $snippet = $this->snippetFromMethod(BatchSnapshot::class, 'partitionRead'); $snippet->addLocal('snapshot', $this->snapshot); $res = $snippet->invoke('partitions'); $this->assertContainsOnlyInstancesOf(PartitionInterface::class, $res->returnVal()); } - public function providePartitionMethods() + public function testPartitionQuery() { - return [['partitionRead'],['partitionQuery']]; + $this->spannerClient->partitionQuery( + Argument::type(PartitionQueryRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new PartitionResponse([ + 'partitions' => [ + new Partition(['partition_token' => 'foo']) + ] + ])); + + $snippet = $this->snippetFromMethod(BatchSnapshot::class, 'partitionQuery'); + $snippet->addLocal('snapshot', $this->snapshot); + + $res = $snippet->invoke('partitions'); + $this->assertContainsOnlyInstancesOf(PartitionInterface::class, $res->returnVal()); } public function testExecutePartition() @@ -171,25 +197,31 @@ public function testExecutePartition() $opts = []; $partition = new QueryPartition($token, $sql, $opts); - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'loginCount', - 'type' => [ - 'code' => Database::TYPE_INT64 + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([$this->serializer->decodeMessage( + new PartialResultSet(), + [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'loginCount', + 'type' => [ + 'code' => Database::TYPE_INT64 + ] ] ] ] + ], + 'values' => [ + ['numberValue' => 0] ] - ], - 'values' => [0] - ])); - - $this->refreshOperation($this->snapshot, $this->connection->reveal()); + ] + )])); $snippet = $this->snippetFromMethod(BatchSnapshot::class, 'executePartition'); $snippet->addLocal('snapshot', $this->snapshot); diff --git a/Spanner/tests/Snippet/Batch/PartitionSharedSnippetTestTrait.php b/Spanner/tests/Snippet/Batch/PartitionSharedSnippetTestTrait.php index 78e53d5cb95c..6339fd0cc9d0 100644 --- a/Spanner/tests/Snippet/Batch/PartitionSharedSnippetTestTrait.php +++ b/Spanner/tests/Snippet/Batch/PartitionSharedSnippetTestTrait.php @@ -42,7 +42,7 @@ public function testClassSerializeExamples($index) public function provideSerializeSnippetIndex() { - return [[1],[2]]; + return [[1], [2]]; } /** diff --git a/Spanner/tests/Snippet/Batch/QueryPartitionTest.php b/Spanner/tests/Snippet/Batch/QueryPartitionTest.php index 37c89a92aac9..f4c8e98b3e66 100644 --- a/Spanner/tests/Snippet/Batch/QueryPartitionTest.php +++ b/Spanner/tests/Snippet/Batch/QueryPartitionTest.php @@ -19,13 +19,21 @@ use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Spanner\Batch\BatchClient; use Google\Cloud\Spanner\Batch\QueryPartition; use Google\Cloud\Spanner\Operation; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use Google\Cloud\Spanner\Timestamp; +use Google\Cloud\Spanner\Serializer; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\Partition; +use Google\Cloud\Spanner\V1\PartitionQueryRequest; +use Google\Cloud\Spanner\V1\PartitionResponse; +use Google\Cloud\Spanner\V1\Session as SessionProto; +use Google\Cloud\Spanner\V1\Transaction; +use Google\Protobuf\Timestamp as TimestampProto; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; /** * @group spanner @@ -33,14 +41,16 @@ */ class QueryPartitionTest extends SnippetTestCase { + use ProphecyTrait; use GrpcTestTrait; use PartitionSharedSnippetTestTrait; - use StubCreationTrait; const DATABASE = 'projects/my-awesome-project/instances/my-instance/databases/my-database'; const SESSION = 'projects/my-awesome-project/instances/my-instance/databases/my-database/sessions/session-id'; const TRANSACTION = 'transaction-id'; + private $spannerClient; + private $serializer; private $className = QueryPartition::class; private $sql = 'SELECT 1=1'; private $time; @@ -49,36 +59,40 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->serializer = new Serializer(); $this->time = time(); $this->partition = new QueryPartition($this->token, $this->sql, $this->options); } public function testClass() { - $connection = $this->getConnStub(); - $connection->createSession(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn([ - 'name' => self::SESSION - ]); - $connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $this->spannerClient->createSession( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + )->willReturn(new SessionProto(['name' => self::SESSION])); + + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new Transaction([ 'id' => self::TRANSACTION, - 'readTimestamp' => \DateTime::createFromFormat('U', (string) $this->time)->format(Timestamp::FORMAT) - ]); - $connection->partitionQuery(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + 'read_timestamp' => new TimestampProto(['seconds' => $this->time]) + ])); + $this->spannerClient->partitionQuery( + Argument::type(PartitionQueryRequest::class), + Argument::type('array') + )->willReturn(new PartitionResponse([ 'partitions' => [ - ['partitionToken' => 'foo'] + new Partition(['partition_token' => 'foo']) ] - ]); + ])); - $client = TestHelpers::stub(BatchClient::class, [ - new Operation($connection->reveal(), false), + $client = new BatchClient( + new Operation($this->spannerClient->reveal(), $this->serializer), self::DATABASE - ]); + ); $snippet = $this->snippetFromClass(QueryPartition::class); $snippet->setLine(3, ''); diff --git a/Spanner/tests/Snippet/Batch/ReadPartitionTest.php b/Spanner/tests/Snippet/Batch/ReadPartitionTest.php index c32e3af55516..bc1543f25d8e 100644 --- a/Spanner/tests/Snippet/Batch/ReadPartitionTest.php +++ b/Spanner/tests/Snippet/Batch/ReadPartitionTest.php @@ -19,14 +19,22 @@ use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Spanner\Batch\BatchClient; use Google\Cloud\Spanner\Batch\ReadPartition; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use Google\Cloud\Spanner\Timestamp; +use Google\Cloud\Spanner\Serializer; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\Partition; +use Google\Cloud\Spanner\V1\PartitionReadRequest; +use Google\Cloud\Spanner\V1\PartitionResponse; +use Google\Cloud\Spanner\V1\Session as SessionProto; +use Google\Cloud\Spanner\V1\Transaction; +use Google\Protobuf\Timestamp as TimestampProto; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; /** * @group spanner @@ -34,16 +42,18 @@ */ class ReadPartitionTest extends SnippetTestCase { + use ProphecyTrait; use GrpcTestTrait; use PartitionSharedSnippetTestTrait { provideGetters as private getters; } - use StubCreationTrait; const DATABASE = 'projects/my-awesome-project/instances/my-instance/databases/my-database'; const SESSION = 'projects/my-awesome-project/instances/my-instance/databases/my-database/sessions/session-id'; const TRANSACTION = 'transaction-id'; + private $spannerClient; + private $serializer; private $className = ReadPartition::class; private $time; private $table; @@ -54,6 +64,8 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->serializer = new Serializer(); $this->time = time(); $this->table = 'table'; $this->keySet = new KeySet(['all' => true]); @@ -63,30 +75,32 @@ public function setUp(): void public function testClass() { - $connection = $this->getConnStub(); - $connection->createSession(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn([ - 'name' => self::SESSION - ]); - $connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $this->spannerClient->createSession( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + )->willReturn(new SessionProto(['name' => self::SESSION])); + + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new Transaction([ 'id' => self::TRANSACTION, - 'readTimestamp' => \DateTime::createFromFormat('U', (string) $this->time)->format(Timestamp::FORMAT) - ]); - $connection->partitionRead(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + 'read_timestamp' => new TimestampProto(['seconds' => $this->time]) + ])); + $this->spannerClient->partitionRead( + Argument::type(PartitionReadRequest::class), + Argument::type('array') + )->willReturn(new PartitionResponse([ 'partitions' => [ - ['partitionToken' => 'foo'] + new Partition(['partition_token' => 'foo']) ] - ]); + ])); - $client = TestHelpers::stub(BatchClient::class, [ - new Operation($connection->reveal(), false), + $client = new BatchClient( + new Operation($this->spannerClient->reveal(), $this->serializer), self::DATABASE - ]); + ); $snippet = $this->snippetFromClass(ReadPartition::class); $snippet->setLine(4, ''); diff --git a/Spanner/tests/Snippet/BatchDmlResultTest.php b/Spanner/tests/Snippet/BatchDmlResultTest.php index f6945b8ca479..c5365ecdc1b7 100644 --- a/Spanner/tests/Snippet/BatchDmlResultTest.php +++ b/Spanner/tests/Snippet/BatchDmlResultTest.php @@ -17,17 +17,24 @@ namespace Google\Cloud\Spanner\Tests\Snippet; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Core\TimeTrait; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; use Google\Cloud\Spanner\BatchDmlResult; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CommitRequest; +use Google\Cloud\Spanner\V1\CommitResponse; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlRequest; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlResponse; +use Google\Cloud\Spanner\V1\Transaction as TransactionProto; +use Google\Protobuf\Timestamp as TimestampProto; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -38,15 +45,18 @@ class BatchDmlResultTest extends SnippetTestCase { use GrpcTestTrait; use ProphecyTrait; - use StubCreationTrait; use TimeTrait; + private $spannerClient; + private $serializer; private $result; public function setUp(): void { $this->checkAndSkipGrpcTests(); + $this->serializer = new Serializer(); + $this->spannerClient = $this->prophesize(SpannerClient::class); $this->result = new BatchDmlResult([ 'resultSets' => [ [ @@ -69,24 +79,23 @@ public function setUp(): void public function testClass() { - $connection = $this->getConnStub(); - $connection->executeBatchDml(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'resultSets' => [] - ]); - - $connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'id' => 'ddfdfd' - ]); - - $connection->commit(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'commitTimestamp' => $this->formatTimeAsString(new \DateTime, 0) - ]); + $this->spannerClient->executeBatchDml( + Argument::type(ExecuteBatchDmlRequest::class), + Argument::type('array') + )->willReturn(new ExecuteBatchDmlResponse(['result_sets' => []])); + + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto(['id' => 'id'])); + + $this->spannerClient->commit( + Argument::type(CommitRequest::class), + Argument::type('array') + )->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $session = $this->prophesize(Session::class); $session->name()->willReturn( @@ -95,7 +104,7 @@ public function testClass() $session->info()->willReturn([ 'databaseName' => 'projects/test-project/instances/my-instance/databases/my-database' ]); - $session->setExpiration(Argument::any())->willReturn(null); + $session->setExpiration(Argument::any()); $sessionPool = $this->prophesize(SessionPoolInterface::class); $sessionPool->acquire(Argument::any()) @@ -108,15 +117,17 @@ public function testClass() $instance->name()->willReturn('projects/test-project/instances/my-instance'); $instance->directedReadOptions()->willReturn([]); - $database = TestHelpers::stub(Database::class, [ - $connection->reveal(), + $databaseAdminClient = $this->prophesize(DatabaseAdminClient::class); + + $database = new Database( + $this->spannerClient->reveal(), + $databaseAdminClient->reveal(), + $this->serializer, $instance->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], 'test-project', 'projects/test-project/instances/my-instance/databases/my-database', - $sessionPool->reveal() - ]); + ['sessionPool' => $sessionPool->reveal()], + ); $snippet = $this->snippetFromClass(BatchDmlResult::class); $snippet->replace('$database = $spanner->connect(\'my-instance\', \'my-database\');', ''); diff --git a/Spanner/tests/Snippet/BytesTest.php b/Spanner/tests/Snippet/BytesTest.php index ea268f6c1554..c75a55efb44f 100644 --- a/Spanner/tests/Snippet/BytesTest.php +++ b/Spanner/tests/Snippet/BytesTest.php @@ -17,10 +17,10 @@ namespace Google\Cloud\Spanner\Tests\Snippet; +use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; use Google\Cloud\Spanner\Bytes; use Google\Cloud\Spanner\Database; -use Google\Cloud\Core\Testing\GrpcTestTrait; use Psr\Http\Message\StreamInterface; /** @@ -63,7 +63,7 @@ public function testGet() $res = $snippet->invoke('stream'); $this->assertInstanceOf(StreamInterface::class, $res->returnVal()); - $this->assertEquals(self::BYTES, (string)$res->returnVal()); + $this->assertEquals(self::BYTES, (string) $res->returnVal()); } public function testType() diff --git a/Spanner/tests/Snippet/CommitTimestampTest.php b/Spanner/tests/Snippet/CommitTimestampTest.php index 1f5dc9ce7f5a..2d2bfa554d0c 100644 --- a/Spanner/tests/Snippet/CommitTimestampTest.php +++ b/Spanner/tests/Snippet/CommitTimestampTest.php @@ -19,12 +19,18 @@ use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Core\Timestamp; use Google\Cloud\Spanner\CommitTimestamp; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\SpannerClient; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient; +use Google\Cloud\Spanner\V1\CommitRequest; +use Google\Cloud\Spanner\V1\CommitResponse; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\DeleteSessionRequest; +use Google\Cloud\Spanner\V1\Session; +use Google\Protobuf\Timestamp as TimestampProto; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; /** * @group spanner @@ -32,13 +38,18 @@ */ class CommitTimestampTest extends SnippetTestCase { + use ProphecyTrait; use GrpcTestTrait; - use StubCreationTrait; const SESSION = 'projects/my-awesome-project/instances/my-instance/databases/my-database/sessions/session-id'; + private $spannerClient; + private $serializer; + public function setUp(): void { + $this->serializer = new Serializer(); + $this->spannerClient = $this->prophesize(GapicSpannerClient::class); $this->checkAndSkipGrpcTests(); } @@ -46,31 +57,49 @@ public function testClass() { $id = 'abc'; - $client = TestHelpers::stub(SpannerClient::class); - $conn = $this->getConnStub(); - $conn->createSession(Argument::any()) - ->willReturn([ - 'name' => self::SESSION - ]); + $this->spannerClient->createSession( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new Session(['name' => self::SESSION])); + $this->spannerClient->deleteSession( + Argument::type(DeleteSessionRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce(); + $this->spannerClient->addMiddleware(Argument::type('callable')) + ->shouldBeCalledOnce(); $mutation = [ 'insert' => [ 'table' => 'myTable', 'columns' => ['id', 'commitTimestamp'], - 'values' => [$id, CommitTimestamp::SPECIAL_VALUE] + 'values' => [[$id, CommitTimestamp::SPECIAL_VALUE]] ] ]; + $this->spannerClient->commit( + Argument::that(function (CommitRequest $request) use ($mutation) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['mutations'][0], $mutation); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); - $conn->commit(Argument::withEntry('mutations', [$mutation]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => \DateTime::createFromFormat('U', (string) time())->format(Timestamp::FORMAT) + $client = new SpannerClient([ + 'projectId' => 'my-project', + 'gapicSpannerClient' => $this->spannerClient->reveal() ]); - $client->___setProperty('connection', $conn->reveal()); - $snippet = $this->snippetFromClass(CommitTimestamp::class); $snippet->addLocal('id', $id); $snippet->addLocal('spanner', $client); - $snippet->replace('$spanner = new SpannerClient();', ''); + $snippet->replace("\$spanner = new SpannerClient(['projectId' => 'my-project']);", ''); $snippet->invoke(); } diff --git a/Spanner/tests/Snippet/DatabaseTest.php b/Spanner/tests/Snippet/DatabaseTest.php index 0f0ab0493e0b..d9f033734b61 100644 --- a/Spanner/tests/Snippet/DatabaseTest.php +++ b/Spanner/tests/Snippet/DatabaseTest.php @@ -17,30 +17,54 @@ namespace Google\Cloud\Spanner\Tests\Snippet; -use Google\Cloud\Core\Iam\Iam; +use Google\ApiCore\OperationResponse; +use Google\ApiCore\Page; +use Google\ApiCore\PagedListResponse; +use Google\Cloud\Core\Iam\IamManager; use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -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\Admin\Database\V1\Backup as BackupProto; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\CreateBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\CreateDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\Database as DatabaseProto; +use Google\Cloud\Spanner\Admin\Database\V1\DropDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseDdlRequest; +use Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseDdlResponse; +use Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupsRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupsResponse; +use Google\Cloud\Spanner\Admin\Database\V1\RestoreDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\UpdateDatabaseDdlRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\Backup; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\Snapshot; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; -use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CommitRequest; +use Google\Cloud\Spanner\V1\CommitResponse; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\PartialResultSet; +use Google\Cloud\Spanner\V1\ReadRequest; +use Google\Cloud\Spanner\V1\ResultSetStats; +use Google\Cloud\Spanner\V1\RollbackRequest; +use Google\Cloud\Spanner\V1\Transaction as TransactionProto; +use Google\LongRunning\Client\OperationsClient; +use Google\LongRunning\ListOperationsResponse; +use Google\LongRunning\Operation; +use Google\Protobuf\Timestamp as TimestampProto; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -51,10 +75,8 @@ class DatabaseTest extends SnippetTestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; use ResultGeneratorTrait; - use StubCreationTrait; const PROJECT = 'my-awesome-project'; const DATABASE = 'my-database'; @@ -62,7 +84,11 @@ class DatabaseTest extends SnippetTestCase const TRANSACTION = 'my-transaction'; const BACKUP = 'my-backup'; - private $connection; + private $spannerClient; + private $databaseAdminClient; + private $instanceAdminClient; + private $operationResponse; + private $serializer; private $database; private $instance; @@ -70,6 +96,12 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->databaseAdminClient = $this->prophesize(DatabaseAdminClient::class); + $this->instanceAdminClient = $this->prophesize(InstanceAdminClient::class); + $this->operationResponse = $this->prophesize(OperationResponse::class); + $this->serializer = new Serializer(); + $session = $this->prophesize(Session::class); $session->info() ->willReturn([ @@ -77,8 +109,7 @@ public function setUp(): void ]); $session->name() ->willReturn('database'); - $session->setExpiration(Argument::any()) - ->willReturn(100); + $session->setExpiration(Argument::any()); $sessionPool = $this->prophesize(SessionPoolInterface::class); $sessionPool->acquire(Argument::any()) @@ -87,24 +118,24 @@ public function setUp(): void ->willReturn(null); $sessionPool->clear()->willReturn(null); - $this->connection = $this->prophesize(ConnectionInterface::class); - $this->instance = TestHelpers::stub(Instance::class, [ - $this->connection->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], + $this->instance = new Instance( + $this->spannerClient->reveal(), + $this->instanceAdminClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, self::PROJECT, self::INSTANCE - ], ['connection', 'lroConnection']); + ); - $this->database = TestHelpers::stub(Database::class, [ - $this->connection->reveal(), + $this->database = new Database( + $this->spannerClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, $this->instance, - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], self::PROJECT, self::DATABASE, - $sessionPool->reveal() - ], ['connection', 'operation', 'lroConnection']); + ['sessionPool' => $sessionPool->reveal()] + ); } public function testClass() @@ -136,11 +167,12 @@ public function testState() $snippet->addLocal('database', $this->database); $snippet->addUse(Database::class); - $this->connection->getDatabase(Argument::any()) - ->shouldBeCalledTimes(1) - ->WillReturn(['state' => Database::STATE_READY]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabase( + Argument::type(GetDatabaseRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new DatabaseProto(['state' => Database::STATE_READY])); $res = $snippet->invoke(); $this->assertEquals('Database is ready!', $res->output()); @@ -154,17 +186,25 @@ public function testBackups() $snippet = $this->snippetFromMethod(Database::class, 'backups'); $snippet->addLocal('database', $this->database); - $this->connection->listBackups(Argument::any()) - ->shouldBeCalled() - ->WillReturn([ - 'backups' => [ - [ - 'name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, self::BACKUP) - ] - ] - ]); + $backup = new BackupProto([ + 'name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, self::BACKUP) + ]); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListBackupsResponse(['backups' => [$backup]])); + $page->getNextPageToken() + ->willReturn(null); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + + $this->databaseAdminClient->listBackups( + Argument::type(ListBackupsRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($pagedListResponse->reveal()); $res = $snippet->invoke('backups'); @@ -180,11 +220,13 @@ public function testCreateBackup() $snippet = $this->snippetFromMethod(Database::class, 'createBackup'); $snippet->addLocal('database', $this->database); - $this->connection->createBackup(Argument::any(), Argument::any()) - ->shouldBeCalled() - ->willReturn(['name' => 'my-operations']); + $this->databaseAdminClient->createBackup( + Argument::type(CreateBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); - $this->database->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); } @@ -205,11 +247,12 @@ public function testExists() $snippet = $this->snippetFromMethod(Database::class, 'exists'); $snippet->addLocal('database', $this->database); - $this->connection->getDatabase(Argument::any()) - ->shouldBeCalled() - ->willReturn(['statements' => []]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabase( + Argument::type(GetDatabaseRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new DatabaseProto(['name' => 'foo'])); $res = $snippet->invoke(); $this->assertEquals('Database exists!', $res->output()); @@ -220,19 +263,18 @@ public function testExists() */ 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()); + $this->databaseAdminClient->getDatabase( + Argument::type(GetDatabaseRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new DatabaseProto(['name' => 'foo'])); $res = $snippet->invoke('info'); - $this->assertEquals($db, $res->returnVal()); + $this->assertEquals('foo', $res->returnVal()['name']); $snippet->invoke(); } @@ -241,19 +283,18 @@ public function testInfo() */ public function testReload() { - $db = ['name' => 'foo']; - $snippet = $this->snippetFromMethod(Database::class, 'reload'); $snippet->addLocal('database', $this->database); - $this->connection->getDatabase(Argument::any()) + $this->databaseAdminClient->getDatabase( + Argument::type(GetDatabaseRequest::class), + Argument::type('array') + ) ->shouldBeCalledTimes(2) - ->willReturn($db); - - $this->database->___setProperty('connection', $this->connection->reveal()); + ->willReturn(new DatabaseProto(['name' => 'foo'])); $res = $snippet->invoke('info'); - $this->assertEquals($db, $res->returnVal()); + $this->assertEquals('foo', $res->returnVal()['name']); $snippet->invoke(); } @@ -265,13 +306,12 @@ 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()); + $this->databaseAdminClient->createDatabase( + Argument::type(CreateDatabaseRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); @@ -287,13 +327,12 @@ public function testRestore() $snippet->addLocal('database', $this->database); $snippet->addLocal('backup', $backup); - $this->connection->restoreDatabase(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->restoreDatabase( + Argument::type(RestoreDatabaseRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); @@ -307,13 +346,12 @@ public function testUpdateDdl() $snippet = $this->snippetFromMethod(Database::class, 'updateDdl'); $snippet->addLocal('database', $this->database); - $this->connection->updateDatabaseDdl(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->updateDatabaseDdl( + Argument::type(UpdateDatabaseDdlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $snippet->invoke(); } @@ -326,13 +364,12 @@ public function testUpdateDdlBatch() $snippet = $this->snippetFromMethod(Database::class, 'updateDdlBatch'); $snippet->addLocal('database', $this->database); - $this->connection->updateDatabaseDdl(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->updateDatabaseDdl( + Argument::type(UpdateDatabaseDdlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $snippet->invoke(); } @@ -345,10 +382,11 @@ 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()); + $this->databaseAdminClient->dropDatabase( + Argument::type(DropDatabaseRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce(); $snippet->invoke(); } @@ -366,13 +404,12 @@ public function testDdl() 'CREATE TABLE TestCases' ]; - $this->connection->getDatabaseDDL(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'statements' => $stmts - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabaseDdl( + Argument::type(GetDatabaseDdlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new GetDatabaseDdlResponse(['statements' => $stmts])); $res = $snippet->invoke('statements'); $this->assertEquals($stmts, $res->returnVal()); @@ -380,13 +417,11 @@ public function testDdl() public function testSnapshot() { - $this->connection->beginTransaction(Argument::any(), Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'id' => self::TRANSACTION - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $snippet = $this->snippetFromMethod(Database::class, 'snapshot'); $snippet->addLocal('database', $this->database); @@ -397,14 +432,14 @@ public function testSnapshot() public function testSnapshotReadTimestamp() { - $this->connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto([ 'id' => self::TRANSACTION, - 'readTimestamp' => (new Timestamp(new \DateTime))->formatAsString() - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + 'read_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $snippet = $this->snippetFromMethod(Database::class, 'snapshot', 1); $snippet->addLocal('database', $this->database); @@ -415,24 +450,25 @@ public function testSnapshotReadTimestamp() 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->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $this->connection->rollback(Argument::any()) - ->shouldNotBeCalled(); + $this->spannerClient->commit( + Argument::type(CommitRequest::class), + Argument::type('array') + )->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->yieldRows([ + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([ [ 'name' => 'loginCount', 'type' => Database::TYPE_INT64, @@ -440,7 +476,7 @@ public function testRunTransaction() ] ])); - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->rollback(Argument::cetera())->shouldNotBeCalled(); $snippet = $this->snippetFromMethod(Database::class, 'runTransaction'); $snippet->addUse(Transaction::class); @@ -453,43 +489,47 @@ public function testRunTransaction() public function testRunTransactionRollback() { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotBeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); - $this->connection->commit(Argument::any()) - ->shouldNotBeCalled(); + $this->spannerClient->commit(Argument::cetera())->shouldNotBeCalled(); - $this->connection->rollback(Argument::any()) - ->shouldBeCalled(); - - $this->connection->executeStreamingSql( - Argument::withEntry('transaction', [ - 'begin' => [ - 'readWrite' => [], - 'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED - ] - ]) + $this->spannerClient->rollback( + Argument::type(RollbackRequest::class), + Argument::type('array') ) - ->shouldBeCalled() - ->willReturn($this->resultGeneratorData([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'timestamp', - 'type' => [ - 'code' => Database::TYPE_TIMESTAMP + ->shouldBeCalledOnce(); + + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) { + $this->assertEquals( + $this->serializer->encodeMessage($request)['transaction']['begin']['readWrite'], + ['readLockMode' => 0, 'multiplexedSessionPreviousTransactionId' => ''] + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([$this->serializer->decodeMessage( + new PartialResultSet(), + [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'timestamp', + 'type' => [ + 'code' => Database::TYPE_TIMESTAMP + ] ] ] + ], + 'transaction' => [ + 'id' => self::TRANSACTION ] - ], - 'transaction' => [ - 'id' => self::TRANSACTION ] ] - ])); - - $this->refreshOperation($this->database, $this->connection->reveal()); + )])); $snippet = $this->snippetFromMethod(Database::class, 'runTransaction'); $snippet->addUse(Transaction::class); @@ -502,13 +542,11 @@ public function testRunTransactionRollback() public function testTransaction() { - $this->connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'id' => self::TRANSACTION - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $snippet = $this->snippetFromMethod(Database::class, 'transaction'); $snippet->addLocal('database', $this->database); @@ -518,37 +556,37 @@ public function testTransaction() public function testInsert() { - $this->connection->commit(Argument::that(function ($args) { - return isset($args['mutations'][0]['insert']); - }))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['insert']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $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->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['insert']) + && isset($message['mutations'][1]['insert']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $snippet = $this->snippetFromMethod(Database::class, 'insertBatch'); $snippet->addLocal('database', $this->database); @@ -557,37 +595,37 @@ public function testInsertBatch() public function testUpdate() { - $this->connection->commit(Argument::that(function ($args) { - return isset($args['mutations'][0]['update']); - }))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['update']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $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->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['update']) + && isset($message['mutations'][1]['update']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $snippet = $this->snippetFromMethod(Database::class, 'updateBatch'); $snippet->addLocal('database', $this->database); @@ -596,37 +634,37 @@ public function testUpdateBatch() public function testInsertOrUpdate() { - $this->connection->commit(Argument::that(function ($args) { - return isset($args['mutations'][0]['insertOrUpdate']); - }))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['insertOrUpdate']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $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->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['insertOrUpdate']) + && isset($message['mutations'][1]['insertOrUpdate']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $snippet = $this->snippetFromMethod(Database::class, 'insertOrUpdateBatch'); $snippet->addLocal('database', $this->database); @@ -635,37 +673,37 @@ public function testInsertOrUpdateBatch() public function testReplace() { - $this->connection->commit(Argument::that(function ($args) { - return isset($args['mutations'][0]['replace']); - }))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['replace']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $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->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['replace']) + && isset($message['mutations'][1]['replace']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $snippet = $this->snippetFromMethod(Database::class, 'replaceBatch'); $snippet->addLocal('database', $this->database); @@ -674,13 +712,17 @@ public function testReplaceBatch() public function testDelete() { - $this->connection->commit(Argument::that(function ($args) { - return isset($args['mutations'][0]['delete']); - }))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['mutations'][0]['delete']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $snippet = $this->snippetFromMethod(Database::class, 'delete'); $snippet->addUse(KeySet::class); @@ -690,11 +732,12 @@ public function testDelete() public function testExecute() { - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $snippet = $this->snippetFromMethod(Database::class, 'execute'); $snippet->addLocal('database', $this->database); @@ -705,37 +748,57 @@ public function testExecute() 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'] !== Database::TYPE_TIMESTAMP) { - return false; - } - - return true; - }))->shouldBeCalled()->willReturn($this->resultGeneratorData([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'timestamp', - 'type' => [ - 'code' => Database::TYPE_TIMESTAMP + $message = $this->serializer->decodeMessage( + new PartialResultSet(), + [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'timestamp', + 'type' => [ + 'code' => Database::TYPE_TIMESTAMP + ] ] ] ] + ], + 'values' => [ + ['nullValue' => 0] ] - ], - 'values' => [null] - ])); - - $this->refreshOperation($this->database, $this->connection->reveal()); + ] + ); + + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['params']) + && isset($message['paramTypes']) + && $message['paramTypes']['timestamp']['code'] === Database::TYPE_TIMESTAMP; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([$this->serializer->decodeMessage( + new PartialResultSet(), + [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'timestamp', + 'type' => [ + 'code' => Database::TYPE_TIMESTAMP + ] + ] + ] + ] + ], + 'values' => [ + ['nullValue' => 0] + ] + ] + )])); $snippet = $this->snippetFromMethod(Database::class, 'execute', 1); $snippet->addLocal('database', $this->database); @@ -746,44 +809,41 @@ public function testExecuteWithParameterType() 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'] !== Database::TYPE_ARRAY) { - return false; - } - - if ($arg['paramTypes']['emptyArrayOfIntegers']['arrayElementType']['code'] !== Database::TYPE_INT64) { - return false; - } - - return true; - }))->shouldBeCalled()->willReturn($this->resultGeneratorData([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'numbers', - 'type' => [ - 'code' => Database::TYPE_ARRAY, - 'arrayElementType' => [ - 'code' => Database::TYPE_INT64 + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return isset($message['params']) + && isset($message['paramTypes']) + && $message['paramTypes']['emptyArrayOfIntegers']['code'] === Database::TYPE_ARRAY + && $message['paramTypes']['emptyArrayOfIntegers']['arrayElementType']['code'] + === Database::TYPE_INT64; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([$this->serializer->decodeMessage( + new PartialResultSet(), + [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'numbers', + 'type' => [ + 'code' => Database::TYPE_ARRAY, + 'arrayElementType' => [ + 'code' => Database::TYPE_INT64 + ] + ] ] ] ] + ], + 'values' => [ + ['listValue' => []] ] ] - ], - 'values' => [[]] - ])); - - $this->refreshOperation($this->database, $this->connection->reveal()); + )])); $snippet = $this->snippetFromMethod(Database::class, 'execute', 2); $snippet->addLocal('database', $this->database); @@ -794,11 +854,12 @@ public function testExecuteWithEmptyArray() public function testExecuteBeginSnapshot() { - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator(false, self::TRANSACTION)); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([], null, self::TRANSACTION)); $snippet = $this->snippetFromMethod(Database::class, 'execute', 5); $snippet->addLocal('database', $this->database); @@ -810,11 +871,12 @@ public function testExecuteBeginSnapshot() public function testExecuteBeginTransaction() { - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator(false, self::TRANSACTION)); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([], null, self::TRANSACTION)); $snippet = $this->snippetFromMethod(Database::class, 'execute', 6); $snippet->addLocal('database', $this->database); @@ -826,17 +888,19 @@ public function testExecuteBeginTransaction() public function testExecutePartitionedUpdate() { - $this->connection->beginTransaction(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'id' => self::TRANSACTION - ]); - - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator(true)); + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $this->refreshOperation($this->database, $this->connection->reveal()); + $stats = new ResultSetStats(['row_count_lower_bound' => 1]); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([], $stats)); $snippet = $this->snippetFromMethod(Database::class, 'executePartitionedUpdate'); $snippet->addLocal('database', $this->database); @@ -847,11 +911,10 @@ public function testExecutePartitionedUpdate() public function testRead() { - $this->connection->streamingRead(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::type(ReadRequest::class), + Argument::type('array') + )->willReturn($this->resultGeneratorStream()); $snippet = $this->snippetFromMethod(Database::class, 'read'); $snippet->addLocal('database', $this->database); @@ -862,11 +925,10 @@ public function testRead() public function testReadWithSnapshot() { - $this->connection->streamingRead(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator(false, self::TRANSACTION)); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::type(ReadRequest::class), + Argument::type('array') + )->willReturn($this->resultGeneratorStream([], null, self::TRANSACTION)); $snippet = $this->snippetFromMethod(Database::class, 'read', 1); $snippet->addLocal('database', $this->database); @@ -878,11 +940,10 @@ public function testReadWithSnapshot() public function testReadWithTransaction() { - $this->connection->streamingRead(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator(false, self::TRANSACTION)); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::type(ReadRequest::class), + Argument::type('array') + )->willReturn($this->resultGeneratorStream([], null, self::TRANSACTION)); $snippet = $this->snippetFromMethod(Database::class, 'read', 2); $snippet->addLocal('database', $this->database); @@ -907,6 +968,7 @@ public function testClose() $snippet->addLocal('database', $this->database); $res = $snippet->invoke(); + $this->assertNull($res->returnVal()); } public function testIam() @@ -915,12 +977,12 @@ public function testIam() $snippet->addLocal('database', $this->database); $res = $snippet->invoke('iam'); - $this->assertInstanceOf(Iam::class, $res->returnVal()); + $this->assertInstanceOf(IamManager::class, $res->returnVal()); } public function testResumeOperation() { - $snippet = $this->snippetFromMagicMethod(Database::class, 'resumeOperation'); + $snippet = $this->snippetFromMethod(Database::class, 'resumeOperation'); $snippet->addLocal('database', $this->database); $snippet->addLocal('operationName', 'foo'); @@ -934,18 +996,23 @@ 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' - ] - ] - ]); + $operation = new Operation(); + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListOperationsResponse(['operations' => [$operation]])); + $page->getNextPageToken() + ->willReturn(null); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + + $operationsClient = $this->prophesize(OperationsClient::class); + $operationsClient->listOperations(Argument::cetera()) + ->willReturn($pagedListResponse->reveal()); - $this->database->___setProperty('lroConnection', $lroConnection->reveal()); + $this->databaseAdminClient->getOperationsClient() + ->shouldBeCalledOnce() + ->willReturn($operationsClient->reveal()); $res = $snippet->invoke('operations'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); diff --git a/Spanner/tests/Snippet/DateTest.php b/Spanner/tests/Snippet/DateTest.php index d73aa7dc9ab0..54971ee2b429 100644 --- a/Spanner/tests/Snippet/DateTest.php +++ b/Spanner/tests/Snippet/DateTest.php @@ -17,10 +17,10 @@ namespace Google\Cloud\Spanner\Tests\Snippet; +use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Date; -use Google\Cloud\Core\Testing\GrpcTestTrait; /** * @group spanner @@ -36,7 +36,7 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->dt = new \DateTimeImmutable; + $this->dt = new \DateTimeImmutable(); $this->date = new Date($this->dt); } diff --git a/Spanner/tests/Snippet/DurationTest.php b/Spanner/tests/Snippet/DurationTest.php deleted file mode 100644 index 58b0fab6a1d4..000000000000 --- a/Spanner/tests/Snippet/DurationTest.php +++ /dev/null @@ -1,85 +0,0 @@ -checkAndSkipGrpcTests(); - - $this->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/Spanner/tests/Snippet/InstanceConfigurationTest.php b/Spanner/tests/Snippet/InstanceConfigurationTest.php index af867e2b8a8c..b584cf84e426 100644 --- a/Spanner/tests/Snippet/InstanceConfigurationTest.php +++ b/Spanner/tests/Snippet/InstanceConfigurationTest.php @@ -17,14 +17,18 @@ namespace Google\Cloud\Spanner\Tests\Snippet; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\ApiCore\OperationResponse; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\CreateInstanceConfigRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\DeleteInstanceConfigRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\GetInstanceConfigRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; +use Google\Cloud\Spanner\Admin\Instance\V1\UpdateInstanceConfigRequest; use Google\Cloud\Spanner\InstanceConfiguration; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Serializer; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -36,31 +40,36 @@ class InstanceConfigurationTest extends SnippetTestCase { use GrpcTestTrait; use ProphecyTrait; - use StubCreationTrait; const PROJECT = 'my-awesome-project'; const CONFIG = 'regional-europe-west'; - private $connection; + private $instanceAdminClient; + private $operationResponse; + private $serializer; private $config; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); - $this->config = TestHelpers::stub(InstanceConfiguration::class, [ - $this->connection->reveal(), + $this->serializer = new Serializer(); + $this->instanceAdminClient = $this->prophesize(InstanceAdminClient::class); + $this->operationResponse = $this->prophesize(OperationResponse::class); + + $this->config = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, self::PROJECT, self::CONFIG, - [], - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - ], ['connection', 'lroConnection']); + ); } public function testClass() { $snippet = $this->snippetFromClass(InstanceConfiguration::class); + $snippet->addLocal('projectId', self::PROJECT); + $res = $snippet->invoke('configuration'); $this->assertInstanceOf(InstanceConfiguration::class, $res->returnVal()); @@ -73,18 +82,19 @@ public function testClass() public function testCreate() { $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'create'); - $this->connection->createInstanceConfig(Argument::any()) - ->shouldBeCalled() - ->willReturn(['name' => 'operations/foo']); - $dummyConnection = $this->connection->reveal(); + $this->instanceAdminClient->createInstanceConfig( + Argument::type(CreateInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); + $baseConfig = new InstanceConfiguration( - $dummyConnection, + $this->instanceAdminClient->reveal(), + $this->serializer, self::PROJECT, self::CONFIG, - [], - $this->prophesize(LongRunningConnectionInterface::class)->reveal() ); - $this->config->___setProperty('connection', $dummyConnection); $snippet->addLocal('baseConfig', $baseConfig); $snippet->addLocal('options', []); $snippet->addLocal('instanceConfig', $this->config); @@ -98,13 +108,13 @@ public function testUpdate() $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'update'); $snippet->addLocal('instanceConfig', $this->config); - $this->connection->updateInstanceConfig(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); + $this->instanceAdminClient->updateInstanceConfig( + Argument::type(UpdateInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); - $this->config->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -113,10 +123,12 @@ public function testDelete() $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'delete'); $snippet->addLocal('instanceConfig', $this->config); - $this->connection->deleteInstanceConfig(Argument::any()) - ->shouldBeCalled(); + $this->instanceAdminClient->deleteInstanceConfig( + Argument::type(DeleteInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce(); - $this->config->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -139,14 +151,19 @@ public function testInfo() 'displayName' => self::CONFIG ]; - $this->connection->getInstanceConfig(Argument::any()) - ->shouldBeCalled() - ->willReturn($info); - - $this->config->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->getInstanceConfig( + Argument::type(GetInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceConfig([ + 'name' => $info['name'], + 'display_name' => $info['displayName'] + ])); $res = $snippet->invoke('info'); - $this->assertEquals($info, $res->returnVal()); + $this->assertEquals($info['name'], $res->returnVal()['name']); + $this->assertEquals($info['displayName'], $res->returnVal()['displayName']); } public function testExists() @@ -154,14 +171,15 @@ public function testExists() $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'exists'); $snippet->addLocal('configuration', $this->config); - $this->connection->getInstanceConfig(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $this->instanceAdminClient->getInstanceConfig( + Argument::type(GetInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceConfig([ 'name' => InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG), - 'displayName' => self::CONFIG - ]); - - $this->config->___setProperty('connection', $this->connection->reveal()); + 'display_name' => self::CONFIG + ])); $res = $snippet->invoke(); $this->assertEquals('Configuration exists!', $res->output()); @@ -177,13 +195,18 @@ public function testReload() $snippet = $this->snippetFromMethod(InstanceConfiguration::class, 'reload'); $snippet->addLocal('configuration', $this->config); - $this->connection->getInstanceConfig(Argument::any()) - ->shouldBeCalled() - ->willReturn($info); - - $this->config->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->getInstanceConfig( + Argument::type(GetInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceConfig([ + 'name' => $info['name'], + 'display_name' => $info['displayName'] + ])); $res = $snippet->invoke('info'); - $this->assertEquals($info, $res->returnVal()); + $this->assertEquals($info['name'], $res->returnVal()['name']); + $this->assertEquals($info['displayName'], $res->returnVal()['displayName']); } } diff --git a/Spanner/tests/Snippet/InstanceTest.php b/Spanner/tests/Snippet/InstanceTest.php index 71cb7897e01c..6b04e6c55ade 100644 --- a/Spanner/tests/Snippet/InstanceTest.php +++ b/Spanner/tests/Snippet/InstanceTest.php @@ -17,21 +17,42 @@ namespace Google\Cloud\Spanner\Tests\Snippet; -use Google\Cloud\Core\Iam\Iam; +use Google\ApiCore\OperationResponse; +use Google\ApiCore\Page; +use Google\ApiCore\PagedListResponse; +use Google\Cloud\Core\Iam\IamManager; use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -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\Admin\Database\V1\Backup as BackupProto; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\CreateDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\Database as DatabaseProto; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupOperationsRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupOperationsResponse; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupsRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupsResponse; +use Google\Cloud\Spanner\Admin\Database\V1\ListDatabaseOperationsRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListDatabaseOperationsResponse; +use Google\Cloud\Spanner\Admin\Database\V1\ListDatabasesRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListDatabasesResponse; +use Google\Cloud\Spanner\Admin\Database\V1\RestoreDatabaseRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\CreateInstanceRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\DeleteInstanceRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\GetInstanceRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\Instance as InstanceProto; +use Google\Cloud\Spanner\Admin\Instance\V1\UpdateInstanceRequest; use Google\Cloud\Spanner\Backup; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\InstanceConfiguration; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Serializer; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\LongRunning\Client\OperationsClient; +use Google\LongRunning\ListOperationsResponse; +use Google\LongRunning\Operation; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -43,7 +64,6 @@ class InstanceTest extends SnippetTestCase { use GrpcTestTrait; use ProphecyTrait; - use StubCreationTrait; const PROJECT = 'my-awesome-project'; const INSTANCE = 'my-instance'; @@ -51,26 +71,46 @@ class InstanceTest extends SnippetTestCase const BACKUP = 'my-backup'; const OPERATION = 'my-operation'; - private $connection; + private $spannerClient; + private $instanceAdminClient; + private $databaseAdminClient; + private $serializer; private $instance; + private $operationResponse; + private $page; + private $pagedListResponse; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); - $this->instance = TestHelpers::stub(Instance::class, [ - $this->connection->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->instanceAdminClient = $this->prophesize(InstanceAdminClient::class); + $this->databaseAdminClient = $this->prophesize(DatabaseAdminClient::class); + $this->operationResponse = $this->prophesize(OperationResponse::class); + + $this->page = $this->prophesize(Page::class); + $this->page->getNextPageToken() + ->willReturn(null); + $this->pagedListResponse = $this->prophesize(PagedListResponse::class); + $this->pagedListResponse->getPage() + ->willReturn($this->page->reveal()); + + $this->serializer = new Serializer(); + $this->instance = new Instance( + $this->spannerClient->reveal(), + $this->instanceAdminClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, self::PROJECT, self::INSTANCE - ], ['connection', 'lroConnection']); + ); } public function testClass() { $snippet = $this->snippetFromClass(Instance::class); + $snippet->addLocal('projectId', self::PROJECT); $res = $snippet->invoke('instance'); $this->assertInstanceOf(Instance::class, $res->returnVal()); $this->assertEquals( @@ -91,11 +131,12 @@ public function testCreate() $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()); + $this->instanceAdminClient->createInstance( + Argument::type(CreateInstanceRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); @@ -115,11 +156,12 @@ 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()); + $this->instanceAdminClient->getInstance( + Argument::type(GetInstanceRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceProto(['node_count' => 1])); $res = $snippet->invoke(); $this->assertEquals('1', $res->output()); @@ -130,11 +172,12 @@ 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()); + $this->instanceAdminClient->getInstance( + Argument::type(GetInstanceRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceProto(['name' => 'foo'])); $res = $snippet->invoke(); $this->assertEquals('Instance exists!', $res->output()); @@ -145,11 +188,12 @@ 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()); + $this->instanceAdminClient->getInstance( + Argument::type(GetInstanceRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceProto(['node_count' => 1])); $res = $snippet->invoke('info'); $info = $this->instance->info(); @@ -162,11 +206,12 @@ public function testState() $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()); + $this->instanceAdminClient->getInstance( + Argument::type(GetInstanceRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceProto(['state' => Instance::STATE_READY])); $res = $snippet->invoke(); $this->assertEquals('Instance is ready!', $res->output()); @@ -177,13 +222,13 @@ public function testUpdate() $snippet = $this->snippetFromMethod(Instance::class, 'update'); $snippet->addLocal('instance', $this->instance); - $this->connection->updateInstance(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); + $this->instanceAdminClient->updateInstance( + Argument::type(UpdateInstanceRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); - $this->instance->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -192,10 +237,12 @@ public function testDelete() $snippet = $this->snippetFromMethod(Instance::class, 'delete'); $snippet->addLocal('instance', $this->instance); - $this->connection->deleteInstance(Argument::any()) - ->shouldBeCalled(); + $this->instanceAdminClient->deleteInstance( + Argument::type(DeleteInstanceRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce(); - $this->instance->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -204,13 +251,12 @@ public function testCreateDatabase() $snippet = $this->snippetFromMethod(Instance::class, 'createDatabase'); $snippet->addLocal('instance', $this->instance); - $this->connection->createDatabase(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->createDatabase( + Argument::type(CreateDatabaseRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); @@ -223,13 +269,12 @@ public function testCreateDatabaseFromBackup() $snippet->addLocal('instance', $this->instance); $snippet->addLocal('backup', $backup); - $this->connection->restoreDatabase(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->restoreDatabase( + Argument::type(RestoreDatabaseRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); @@ -250,17 +295,23 @@ public function testDatabases() $snippet = $this->snippetFromMethod(Instance::class, 'databases'); $snippet->addLocal('instance', $this->instance); - $this->connection->listDatabases(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'databases' => [ - [ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) - ] - ] - ]); + $database = new DatabaseProto([ + 'name' => DatabaseAdminClient::databaseName( + self::PROJECT, + self::INSTANCE, + self::DATABASE + ) + ]); + + $this->page->getResponseObject() + ->willReturn(new ListDatabasesResponse(['databases' => [$database]])); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->listDatabases( + Argument::type(ListDatabasesRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->pagedListResponse->reveal()); $res = $snippet->invoke('databases'); @@ -283,17 +334,23 @@ public function testBackups() $snippet = $this->snippetFromMethod(Instance::class, 'backups'); $snippet->addLocal('instance', $this->instance); - $this->connection->listBackups(Argument::any()) - ->shouldBeCalled() - ->WillReturn([ - 'backups' => [ - [ - 'name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, self::BACKUP) - ] - ] - ]); + $backup = new BackupProto([ + 'name' => DatabaseAdminClient::backupName( + self::PROJECT, + self::INSTANCE, + self::BACKUP + ) + ]); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->page->getResponseObject() + ->willReturn(new ListBackupsResponse(['backups' => [$backup]])); + + $this->databaseAdminClient->listBackups( + Argument::type(ListBackupsRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->pagedListResponse->reveal()); $res = $snippet->invoke('backups'); @@ -304,25 +361,24 @@ public function testBackups() public function testBackupOperations() { $backupOperationName = sprintf( - "%s/operations/%s", + '%s/operations/%s', DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, self::BACKUP), self::OPERATION ); + $operation = new Operation(['name' => $backupOperationName]); + $this->page->getResponseObject() + ->willReturn(new ListBackupOperationsResponse(['operations' => [$operation]])); + $snippet = $this->snippetFromMethod(Instance::class, 'backupOperations'); $snippet->addLocal('instance', $this->instance); - $this->connection->listBackupOperations(Argument::any()) - ->shouldBeCalled() - ->WillReturn([ - 'operations' => [ - [ - 'name' => $backupOperationName - ] - ] - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->listBackupOperations( + Argument::type(ListBackupOperationsRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->pagedListResponse->reveal()); $res = $snippet->invoke('backupOperations'); @@ -333,25 +389,24 @@ public function testBackupOperations() public function testDatabaseOperations() { $databaseOperationName = sprintf( - "%s/operations/%s", + '%s/operations/%s', DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE), self::OPERATION ); + $operation = new Operation(['name' => $databaseOperationName]); + $this->page->getResponseObject() + ->willReturn(new ListDatabaseOperationsResponse(['operations' => [$operation]])); + $snippet = $this->snippetFromMethod(Instance::class, 'databaseOperations'); $snippet->addLocal('instance', $this->instance); - $this->connection->listDatabaseOperations(Argument::any()) - ->shouldBeCalled() - ->WillReturn([ - 'operations' => [ - [ - 'name' => $databaseOperationName - ] - ] - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->listDatabaseOperations( + Argument::type(ListDatabaseOperationsRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->pagedListResponse->reveal()); $res = $snippet->invoke('databaseOperations'); @@ -365,7 +420,7 @@ public function testIam() $snippet->addLocal('instance', $this->instance); $res = $snippet->invoke('iam'); - $this->assertInstanceOf(Iam::class, $res->returnVal()); + $this->assertInstanceOf(IamManager::class, $res->returnVal()); } public function testResumeOperation() @@ -384,18 +439,23 @@ 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()); + $operation = new Operation(); + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListOperationsResponse(['operations' => [$operation]])); + $page->getNextPageToken() + ->willReturn(null); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + + $operationsClient = $this->prophesize(OperationsClient::class); + $operationsClient->listOperations(Argument::cetera()) + ->willReturn($pagedListResponse->reveal()); + + $this->instanceAdminClient->getOperationsClient() + ->shouldBeCalledOnce() + ->willReturn($operationsClient->reveal()); $res = $snippet->invoke('operations'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); diff --git a/Spanner/tests/Snippet/KeyRangeTest.php b/Spanner/tests/Snippet/KeyRangeTest.php index 6bfe45eb6c7c..905408e022d8 100644 --- a/Spanner/tests/Snippet/KeyRangeTest.php +++ b/Spanner/tests/Snippet/KeyRangeTest.php @@ -17,9 +17,9 @@ namespace Google\Cloud\Spanner\Tests\Snippet; +use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; use Google\Cloud\Spanner\KeyRange; -use Google\Cloud\Core\Testing\GrpcTestTrait; /** * @group spanner @@ -34,7 +34,7 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->range = new KeyRange; + $this->range = new KeyRange(); } public function testClass() diff --git a/Spanner/tests/Snippet/KeySetTest.php b/Spanner/tests/Snippet/KeySetTest.php index 6134a7f730a9..784d9a948211 100644 --- a/Spanner/tests/Snippet/KeySetTest.php +++ b/Spanner/tests/Snippet/KeySetTest.php @@ -17,10 +17,10 @@ namespace Google\Cloud\Spanner\Tests\Snippet; +use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Core\Testing\GrpcTestTrait; /** * @group spanner diff --git a/Spanner/tests/Snippet/NumericTest.php b/Spanner/tests/Snippet/NumericTest.php index f7e4fe753cc9..921d92143080 100644 --- a/Spanner/tests/Snippet/NumericTest.php +++ b/Spanner/tests/Snippet/NumericTest.php @@ -19,7 +19,6 @@ use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; use Google\Cloud\Spanner\Numeric; -use Google\Cloud\Core\Testing\GrpcTestTrait; /** * @group spanner diff --git a/Spanner/tests/Snippet/ResultTest.php b/Spanner/tests/Snippet/ResultTest.php index c00cf77f3d2e..959023ed571f 100644 --- a/Spanner/tests/Snippet/ResultTest.php +++ b/Spanner/tests/Snippet/ResultTest.php @@ -17,13 +17,13 @@ namespace Google\Cloud\Spanner\Tests\Snippet; +use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Result; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Snapshot; use Google\Cloud\Spanner\Transaction; -use Google\Cloud\Core\Testing\GrpcTestTrait; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -45,7 +45,7 @@ public function setUp(): void $result = $this->prophesize(Result::class); $database = $this->prophesize(Database::class); $result->rows() - ->willReturn($this->resultGenerator()); + ->willReturn($this->resultGeneratorStream()); $result->metadata() ->willReturn([]); $result->columns() @@ -116,7 +116,9 @@ public function testStats() public function testQueryWithStats() { $db = $this->prophesize(Database::class); - $db->execute(Argument::any(), ['queryMode' => 'PROFILE']); + $db->execute(Argument::any(), ['queryMode' => Result::MODE_PROFILE]) + ->shouldBeCalledOnce() + ->willReturn($this->prophesize(Result::class)->reveal()); $snippet = $this->snippetFromMethod(Result::class, 'stats', 1); $snippet->addLocal('database', $db->reveal()); @@ -139,7 +141,7 @@ public function testTransaction() $this->assertInstanceOf(Transaction::class, $res->returnVal()); } - private function resultGenerator() + private function resultGeneratorStream() { yield []; } diff --git a/Spanner/tests/Snippet/Session/CacheSessionPoolTest.php b/Spanner/tests/Snippet/Session/CacheSessionPoolTest.php index e54b6e6e646b..2496c418cc84 100644 --- a/Spanner/tests/Snippet/Session/CacheSessionPoolTest.php +++ b/Spanner/tests/Snippet/Session/CacheSessionPoolTest.php @@ -35,7 +35,7 @@ public function testClass() $snippet = $this->snippetFromClass(CacheSessionPool::class); $snippet->replace('$cache =', '//$cache ='); - $snippet->addLocal('cache', new MemoryCacheItemPool); + $snippet->addLocal('cache', new MemoryCacheItemPool()); $res = $snippet->invoke('database'); $this->assertInstanceOf(Database::class, $res->returnVal()); } @@ -48,8 +48,9 @@ public function testClassLabels() $snippet = $this->snippetFromClass(CacheSessionPool::class, 1); $snippet->replace('$cache =', '//$cache ='); - $snippet->addLocal('cache', new MemoryCacheItemPool); - $res = $snippet->invoke(); + $snippet->addLocal('cache', new MemoryCacheItemPool()); + $res = $snippet->invoke('sessionPool'); + $this->assertInstanceOf(CacheSessionPool::class, $res->returnVal()); } public function testClassWithDatabaseRole() @@ -60,7 +61,7 @@ public function testClassWithDatabaseRole() $snippet = $this->snippetFromClass(CacheSessionPool::class, 2); $snippet->replace('$cache =', '//$cache ='); - $snippet->addLocal('cache', new MemoryCacheItemPool); + $snippet->addLocal('cache', new MemoryCacheItemPool()); $res = $snippet->invoke('database'); $this->assertInstanceOf(Database::class, $res->returnVal()); } diff --git a/Spanner/tests/Snippet/SnapshotTest.php b/Spanner/tests/Snippet/SnapshotTest.php index f66b13171519..5ef4810e437b 100644 --- a/Spanner/tests/Snippet/SnapshotTest.php +++ b/Spanner/tests/Snippet/SnapshotTest.php @@ -19,13 +19,11 @@ use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Operation; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Snapshot; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; use Google\Cloud\Spanner\Timestamp; use Prophecy\PhpUnit\ProphecyTrait; @@ -35,44 +33,46 @@ class SnapshotTest extends SnippetTestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; - use StubCreationTrait; const TRANSACTION = 'my-transaction'; - private $connection; + private $spannerClient; + private $serializer; private $snapshot; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); + $this->serializer = new Serializer(); $operation = $this->prophesize(Operation::class); $session = $this->prophesize(Session::class); - $this->snapshot = TestHelpers::stub(Snapshot::class, [ + $this->snapshot = new Snapshot( $operation->reveal(), $session->reveal(), [ 'id' => self::TRANSACTION, - 'readTimestamp' => new Timestamp(new \DateTime) + 'readTimestamp' => new Timestamp(new \DateTime()) ] - ], ['operation']); + ); } public function testClass() { + $snapshot = $this->prophesize(Snapshot::class)->reveal(); $database = $this->prophesize(Database::class); - $database->snapshot()->shouldBeCalled()->willReturn('foo'); + $database->snapshot() + ->shouldBeCalled() + ->willReturn($snapshot); $snippet = $this->snippetFromClass(Snapshot::class); $snippet->replace('$database =', '//$database ='); $snippet->addLocal('database', $database->reveal()); $res = $snippet->invoke('transaction'); - $this->assertEquals('foo', $res->returnVal()); + $this->assertEquals($snapshot, $res->returnVal()); } public function testId() diff --git a/Spanner/tests/Snippet/SpannerClientTest.php b/Spanner/tests/Snippet/SpannerClientTest.php index 564f75698cc1..9c643e272940 100644 --- a/Spanner/tests/Snippet/SpannerClientTest.php +++ b/Spanner/tests/Snippet/SpannerClientTest.php @@ -17,31 +17,41 @@ namespace Google\Cloud\Spanner\Tests\Snippet; +use Google\ApiCore\OperationResponse; +use Google\ApiCore\Page; +use Google\ApiCore\PagedListResponse; use Google\Cloud\Core\Int64; use Google\Cloud\Core\Iterator\ItemIterator; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\CreateInstanceRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\Instance as InstanceProto; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstanceConfigsRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstanceConfigsResponse; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstancesRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstancesResponse; use Google\Cloud\Spanner\Batch\BatchClient; use Google\Cloud\Spanner\Bytes; use Google\Cloud\Spanner\CommitTimestamp; 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; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Numeric; +use Google\Cloud\Spanner\PgJsonb; use Google\Cloud\Spanner\PgNumeric; use Google\Cloud\Spanner\PgOid; -use Google\Cloud\Spanner\PgJsonb; +use Google\Cloud\Spanner\Serializer; +use Google\Cloud\Spanner\SpannerClient; +use Google\Cloud\Spanner\Timestamp; +use Google\Protobuf\Duration; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; /** * @group spanner @@ -49,22 +59,29 @@ class SpannerClientTest extends SnippetTestCase { use GrpcTestTrait; - use StubCreationTrait; + use ProphecyTrait; const PROJECT = 'my-awesome-project'; const CONFIG = 'foo'; const INSTANCE = 'my-instance'; private $client; - private $connection; + private $spannerClient; + private $instanceAdminClient; + private $operationResponse; + private $serializer; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); - $this->client = TestHelpers::stub(SpannerClient::class); - $this->client->___setProperty('connection', $this->connection->reveal()); + $this->serializer = new Serializer(); + $this->instanceAdminClient = $this->prophesize(InstanceAdminClient::class); + $this->client = new SpannerClient([ + 'projectId' => self::PROJECT, + 'gapicSpannerInstanceAdminClient' => $this->instanceAdminClient->reveal(), + ]); + $this->operationResponse = $this->prophesize(OperationResponse::class); } public function testClass() @@ -87,16 +104,26 @@ public function testBatch() */ public function testInstanceConfigurations() { - $this->connection->listInstanceConfigs(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'instanceConfigs' => [ - ['name' => 'projects/my-awesome-projects/instanceConfigs/foo'], - ['name' => 'projects/my-awesome-projects/instanceConfigs/bar'], + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListInstanceConfigsResponse([ + 'instance_configs' => [ + new InstanceConfig(['name' => 'projects/my-awesome-projects/instanceConfigs/foo']), + new InstanceConfig(['name' => 'projects/my-awesome-projects/instanceConfigs/bar']), ] - ]); - - $this->client->___setProperty('connection', $this->connection->reveal()); + ])); + $page->getNextPageToken() + ->willReturn(null); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + + $this->instanceAdminClient->listInstanceConfigs( + Argument::type(ListInstanceConfigsRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($pagedListResponse->reveal()); $snippet = $this->snippetFromMethod(SpannerClient::class, 'instanceConfigurations'); $snippet->addLocal('spanner', $this->client); @@ -136,11 +163,12 @@ public function testCreateInstance() $snippet->addLocal('spanner', $this->client); $snippet->addLocal('configuration', $this->client->instanceConfiguration(self::CONFIG)); - $this->connection->createInstance(Argument::any()) - ->shouldBeCalled() - ->willReturn(['name' => 'operations/foo']); - - $this->client->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->createInstance( + Argument::type(CreateInstanceRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->prophesize(OperationResponse::class)->reveal()); $res = $snippet->invoke('operation'); $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); @@ -170,16 +198,26 @@ public function testInstances() $snippet = $this->snippetFromMethod(SpannerClient::class, 'instances'); $snippet->addLocal('spanner', $this->client); - $this->connection->listInstances(Argument::any()) - ->shouldBeCalled() - ->willReturn([ + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListInstancesResponse([ 'instances' => [ - ['name' => InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)], - ['name' => InstanceAdminClient::instanceName(self::PROJECT, 'bar')] + new InstanceProto(['name' => InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)]), + new InstanceProto(['name' => InstanceAdminClient::instanceName(self::PROJECT, 'bar')]) ] - ]); - - $this->client->___setProperty('connection', $this->connection->reveal()); + ])); + $page->getNextPageToken() + ->willReturn(null); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + + $this->instanceAdminClient->listInstances( + Argument::type(ListInstancesRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($pagedListResponse->reveal()); $res = $snippet->invoke('instances'); $this->assertInstanceOf(ItemIterator::class, $res->returnVal()); @@ -266,16 +304,6 @@ public function factoriesProvider() ]; } - public function testResumeOperation() - { - $snippet = $this->snippetFromMagicMethod(SpannerClient::class, 'resumeOperation'); - $snippet->addLocal('spanner', $this->client); - $snippet->addLocal('operationName', 'operations/foo'); - - $res = $snippet->invoke('operation'); - $this->assertInstanceOf(LongRunningOperation::class, $res->returnVal()); - } - public function testEmulator() { $snippet = $this->snippetFromClass(SpannerClient::class, 1); diff --git a/Spanner/tests/Snippet/StructTypeTest.php b/Spanner/tests/Snippet/StructTypeTest.php index bbd1f7bc08ef..cdcfb14fc12a 100644 --- a/Spanner/tests/Snippet/StructTypeTest.php +++ b/Spanner/tests/Snippet/StructTypeTest.php @@ -17,19 +17,19 @@ namespace Google\Cloud\Spanner\Tests\Snippet; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\ArrayType; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\StructType; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; +use Google\Cloud\Spanner\V1\Client\SpannerClient; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -40,15 +40,15 @@ class StructTypeTest extends SnippetTestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; - use StubCreationTrait; + use ResultGeneratorTrait; const PROJECT = 'my-awesome-project'; const DATABASE = 'my-database'; const INSTANCE = 'my-instance'; - private $connection; + private $spannerClient; + private $serializer; private $database; private $type; @@ -56,6 +56,8 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $instance = $this->prophesize(Instance::class); $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); $instance->directedReadOptions()->willReturn([]); @@ -67,8 +69,7 @@ public function setUp(): void ]); $session->name() ->willReturn('database'); - $session->setExpiration(Argument::any()) - ->willReturn(100); + $session->setExpiration(Argument::any()); $sessionPool = $this->prophesize(SessionPoolInterface::class); $sessionPool->acquire(Argument::any()) @@ -76,77 +77,71 @@ public function setUp(): void $sessionPool->setDatabase(Argument::any()) ->willReturn(null); - $this->connection = $this->getConnStub(); - $this->database = TestHelpers::stub(Database::class, [ - $this->connection->reveal(), + $this->serializer = new Serializer(); + $this->database = new Database( + $this->spannerClient->reveal(), + $this->prophesize(DatabaseAdminClient::class)->reveal(), + $this->serializer, $instance->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], self::PROJECT, self::DATABASE, - $sessionPool->reveal() - ], ['operation']); + ['sessionPool' => $sessionPool->reveal()] + ); - $this->type = new StructType; + $this->type = new StructType(); } public function testExecuteStruct() { - $fields = [ + $rows = [ [ 'name' => 'firstName', - 'type' => [ - 'code' => Database::TYPE_STRING - ] + 'type' => Database::TYPE_STRING, + 'value' => 'John', ], [ 'name' => 'lastName', - 'type' => [ - 'code' => Database::TYPE_STRING - ] + 'type' => Database::TYPE_STRING, + 'value' => 'Testuser', ] ]; - $values = [ - 'John', - 'Testuser' - ]; - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', 'SELECT @userStruct.firstName, @userStruct.lastName'), - Argument::withEntry('params', [ - 'userStruct' => $values - ]), - Argument::withEntry('paramTypes', [ - 'userStruct' => [ - 'code' => Database::TYPE_STRUCT, - 'structType' => [ - 'fields' => $fields - ] - ] - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => $fields - ] - ], - 'values' => $values - ])); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $stream = $this->resultGeneratorStream($rows); + + $this->spannerClient->executeStreamingSql( + Argument::that(function ($args) use ($rows) { + $message = $this->serializer->encodeMessage($args); + $this->assertEquals('SELECT @userStruct.firstName, @userStruct.lastName', $args->getSql()); + $this->assertEquals( + $message['params']['userStruct'], + array_map(fn ($row) => $row['value'], $rows) + ); + $this->assertEquals( + $message['paramTypes']['userStruct']['structType']['fields'][0]['name'], + $rows[0]['name'] + ); + $this->assertEquals( + $message['paramTypes']['userStruct']['structType']['fields'][1]['name'], + $rows[1]['name'] + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($stream); $snippet = $this->snippetFromClass(StructType::class); $snippet->replace('$database = $spanner->connect(\'my-instance\', \'my-database\');', ''); $snippet->addLocal('database', $this->database); - $res = $snippet->invoke('fullName'); - $this->assertEquals('John Testuser', $res->returnVal()); + $this->assertEquals('John Testuser', $snippet->invoke('fullName')->returnVal()); } public function testConstruct() { $snippet = $this->snippetFromMethod(StructType::class, '__construct'); - $snippet->invoke(); + $structType = $snippet->invoke('structType')->returnVal(); + $this->assertInstanceOf(StructType::class, $structType); } public function testAdd() @@ -177,7 +172,7 @@ public function testAddComplex() [ 'name' => 'customer', 'type' => Database::TYPE_STRUCT, - 'child' => (new StructType) + 'child' => (new StructType()) ->add('name', Database::TYPE_STRING) ->add('phone', Database::TYPE_STRING) ->add('email', Database::TYPE_STRING) @@ -203,9 +198,4 @@ public function testAddUnnamed() ] ], $this->type->fields()); } - - private function resultGenerator(array $data) - { - yield $data; - } } diff --git a/Spanner/tests/Snippet/StructValueTest.php b/Spanner/tests/Snippet/StructValueTest.php index 64eab73f2f23..b1586d71ad94 100644 --- a/Spanner/tests/Snippet/StructValueTest.php +++ b/Spanner/tests/Snippet/StructValueTest.php @@ -17,18 +17,18 @@ namespace Google\Cloud\Spanner\Tests\Snippet; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\StructValue; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; +use Google\Cloud\Spanner\V1\Client\SpannerClient; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -39,15 +39,15 @@ class StructValueTest extends SnippetTestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; - use StubCreationTrait; + use ResultGeneratorTrait; const PROJECT = 'my-awesome-project'; const DATABASE = 'my-database'; const INSTANCE = 'my-instance'; - private $connection; + private $spannerClient; + private $serializer; private $database; private $value; @@ -55,6 +55,8 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $instance = $this->prophesize(Instance::class); $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); $instance->directedReadOptions()->willReturn([]); @@ -66,8 +68,7 @@ public function setUp(): void ]); $session->name() ->willReturn('database'); - $session->setExpiration(Argument::any()) - ->willReturn(100); + $session->setExpiration(Argument::any()); $sessionPool = $this->prophesize(SessionPoolInterface::class); $sessionPool->acquire(Argument::any()) @@ -75,69 +76,67 @@ public function setUp(): void $sessionPool->setDatabase(Argument::any()) ->willReturn(null); - $this->connection = $this->getConnStub(); - $this->database = TestHelpers::stub(Database::class, [ - $this->connection->reveal(), + $this->serializer = new Serializer(); + $this->database = new Database( + $this->spannerClient->reveal(), + $this->prophesize(DatabaseAdminClient::class)->reveal(), + $this->serializer, $instance->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], self::PROJECT, self::DATABASE, - $sessionPool->reveal() - ], ['operation']); + ['sessionPool' => $sessionPool->reveal()] + ); - $this->value = new StructValue; + $this->value = new StructValue(); } public function testConstructor() { - $fields = [ + $rows = [ [ 'name' => 'foo', - 'type' => [ - 'code' => Database::TYPE_STRING - ] + 'type' => Database::TYPE_STRING, + 'value' => 'bar', ], [ 'name' => 'foo', - 'type' => [ - 'code' => Database::TYPE_INT64 - ] + 'type' => Database::TYPE_INT64, + 'value' => 2, ], [ - 'type' => [ - 'code' => Database::TYPE_STRING - ] + 'name' => '', + 'type' => Database::TYPE_STRING, + 'value' => 'this field is unnamed', ] ]; - $values = [ - 'bar', - 2, - 'this field is unnamed' - ]; - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', 'SELECT * FROM UNNEST(ARRAY(SELECT @structParam))'), - Argument::withEntry('params', [ - 'structParam' => $values - ]), - Argument::withEntry('paramTypes', [ - 'structParam' => [ - 'code' => Database::TYPE_STRUCT, - 'structType' => [ - 'fields' => $fields - ] - ] - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => $fields - ] - ], - 'values' => $values - ])); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($args) use ($rows) { + $this->assertEquals( + $args->getSql(), + 'SELECT * FROM UNNEST(ARRAY(SELECT @structParam))' + ); + $message = $this->serializer->encodeMessage($args); + $this->assertEquals( + $message['params']['structParam'], + array_map(fn ($row) => $row['value'], $rows) + ); + $this->assertEquals( + $message['paramTypes']['structParam']['structType']['fields'][0]['name'], + $rows[0]['name'] + ); + $this->assertEquals( + $message['paramTypes']['structParam']['structType']['fields'][1]['name'], + $rows[1]['name'] + ); + $this->assertEquals( + $message['paramTypes']['structParam']['structType']['fields'][2]['name'], + $rows[2]['name'] + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($rows)); $snippet = $this->snippetFromClass(StructValue::class); $snippet->replace('$database = $spanner->connect(\'my-instance\', \'my-database\');', ''); @@ -177,9 +176,4 @@ public function testAddUnnamed() ] ], $this->value->values()); } - - private function resultGenerator(array $data) - { - yield $data; - } } diff --git a/Spanner/tests/Snippet/TimestampTest.php b/Spanner/tests/Snippet/TimestampTest.php index 24be6f0a7256..eac7d4aa263b 100644 --- a/Spanner/tests/Snippet/TimestampTest.php +++ b/Spanner/tests/Snippet/TimestampTest.php @@ -17,10 +17,10 @@ namespace Google\Cloud\Spanner\Tests\Snippet; +use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Timestamp; -use Google\Cloud\Core\Testing\GrpcTestTrait; /** * @group spanner @@ -30,12 +30,13 @@ class TimestampTest extends SnippetTestCase use GrpcTestTrait; private $timestamp; + private $dt; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->dt = new \DateTime; + $this->dt = new \DateTime(); $this->timestamp = new Timestamp($this->dt); } @@ -52,7 +53,7 @@ public function testClassCast() $snippet->addLocal('timestamp', $this->timestamp); $res = $snippet->invoke(); - $this->assertEquals((string)$this->timestamp, $res->output()); + $this->assertEquals((string) $this->timestamp, $res->output()); } public function testGet() diff --git a/Spanner/tests/Snippet/TransactionTest.php b/Spanner/tests/Snippet/TransactionTest.php index 48027c6c44c8..3dd709e52afe 100644 --- a/Spanner/tests/Snippet/TransactionTest.php +++ b/Spanner/tests/Snippet/TransactionTest.php @@ -19,20 +19,29 @@ use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\StructType; use Google\Cloud\Spanner\StructValue; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CommitRequest; +use Google\Cloud\Spanner\V1\CommitResponse; use Google\Cloud\Spanner\V1\CommitResponse\CommitStats; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlRequest; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlResponse; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\ReadRequest; +use Google\Cloud\Spanner\V1\ResultSet; +use Google\Cloud\Spanner\V1\ResultSetStats; +use Google\Cloud\Spanner\V1\RollbackRequest; +use Google\Protobuf\Timestamp as TimestampProto; +use Google\Rpc\Status; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -42,22 +51,22 @@ class TransactionTest extends SnippetTestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; use ResultGeneratorTrait; - use StubCreationTrait; const TRANSACTION = 'my-transaction'; - private $connection; + private $spannerClient; + private $serializer; private $transaction; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); - $operation = $this->prophesize(Operation::class); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->serializer = new Serializer(); + $operation = new Operation($this->spannerClient->reveal(), $this->serializer); $session = $this->prophesize(Session::class); $session->info() ->willReturn([ @@ -66,17 +75,20 @@ public function setUp(): void $session->name() ->willReturn('database'); - $this->transaction = TestHelpers::stub(Transaction::class, [ - $operation->reveal(), + $this->transaction = new Transaction( + $operation, $session->reveal(), - self::TRANSACTION - ], ['operation', 'isRetry']); + self::TRANSACTION, + ['isRetry' => true] + ); } public function testClass() { $database = $this->prophesize(Database::class); - $database->runTransaction(Argument::type('callable'))->shouldBeCalled(); + $database->runTransaction(Argument::type('callable')) + ->shouldBeCalled() + ->willReturn(null); $snippet = $this->snippetFromClass(Transaction::class); $snippet->replace('$database =', '//$database ='); @@ -87,24 +99,26 @@ public function testClass() public function testClassReturnTransaction() { + $transaction = $this->prophesize(Transaction::class)->reveal(); $database = $this->prophesize(Database::class); $database->transaction() ->shouldBeCalled() - ->willReturn('foo'); + ->willReturn($transaction); $snippet = $this->snippetFromClass(Transaction::class, 1); $snippet->addLocal('database', $database->reveal()); $res = $snippet->invoke('transaction'); - $this->assertEquals('foo', $res->returnVal()); + $this->assertEquals($transaction, $res->returnVal()); } public function testExecute() { - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $snippet = $this->snippetFromMagicMethod(Transaction::class, 'execute'); $snippet->addLocal('transaction', $this->transaction); @@ -115,11 +129,13 @@ public function testExecute() public function testExecuteUpdate() { - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator(true)); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $stats = new ResultSetStats(['row_count_exact' => 1]); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream(null, $stats)); $snippet = $this->snippetFromMethod(Transaction::class, 'executeUpdate'); $snippet->addLocal('transaction', $this->transaction); @@ -131,46 +147,45 @@ public function testExecuteUpdate() public function testExecuteUpdateWithStruct() { $expectedSql = "UPDATE Posts SET title = 'Updated Title' WHERE " . - "STRUCT(Title, Content) = @post"; + 'STRUCT<Title STRING, Content STRING>(Title, Content) = @post'; $expectedParams = [ - 'post' => ["Updated Title", "Sample Content"] + 'post' => ['Updated Title', 'Sample Content'] ]; $expectedStructData = [ [ - "name" => "Title", - "type" => [ - "code" => Database::TYPE_STRING + 'name' => 'Title', + 'type' => [ + 'code' => Database::TYPE_STRING, + 'typeAnnotation' => 0, + 'protoTypeFqn' => '' ] ], [ - "name" => "Content", - "type" => [ - "code" => Database::TYPE_STRING + 'name' => 'Content', + 'type' => [ + 'code' => Database::TYPE_STRING, + 'typeAnnotation' => 0, + 'protoTypeFqn' => '' ] ] ]; - $this->connection->executeStreamingSql( - Argument::allOf( - Argument::withEntry('sql', $expectedSql), - Argument::withEntry('params', $expectedParams), - Argument::withEntry( - 'paramTypes', - Argument::withEntry( - 'post', - Argument::withEntry( - 'structType', - Argument::withEntry('fields', Argument::is($expectedStructData)) - ) - ) - ) - ) + $this->spannerClient->executeStreamingSql( + Argument::that(function ($args) use ($expectedSql, $expectedParams, $expectedStructData) { + $message = $this->serializer->encodeMessage($args); + $this->assertEquals($expectedSql, $args->getSql()); + $this->assertEquals($message['params'], $expectedParams); + $this->assertEquals($message['paramTypes']['post']['structType']['fields'], $expectedStructData); + return true; + }), + Argument::type('array') ) - ->shouldBeCalled() - ->willReturn($this->resultGenerator(true)); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream( + [], + new ResultSetStats(['row_count_exact' => 1]) + )); $snippet = $this->snippetFromMethod(Transaction::class, 'executeUpdate', 1); $snippet->addUse(Database::class); @@ -185,19 +200,18 @@ public function testExecuteUpdateWithStruct() public function testExecuteUpdateBatch() { - $this->connection->executeBatchDml(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'resultSets' => [ - [ - 'stats' => [ - 'rowCountExact' => 1 - ] - ] + $this->spannerClient->executeBatchDml( + Argument::type(ExecuteBatchDmlRequest::class), + Argument::type('array') + )->willReturn(new ExecuteBatchDmlResponse([ + 'result_sets' => [ + new ResultSet([ + 'stats' => new ResultSetStats([ + 'row_count_exact' => 1 + ]) + ]) ] - ]); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + ])); $snippet = $this->snippetFromMethod(Transaction::class, 'executeUpdateBatch'); $snippet->addLocal('transaction', $this->transaction); @@ -208,17 +222,16 @@ public function testExecuteUpdateBatch() public function testExecuteUpdateBatchError() { - $this->connection->executeBatchDml(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'resultSets' => [], - 'status' => [ + $this->spannerClient->executeBatchDml( + Argument::type(ExecuteBatchDmlRequest::class), + Argument::type('array') + )->willReturn(new ExecuteBatchDmlResponse([ + 'result_sets' => [], + 'status' => new Status([ 'code' => 3, 'message' => 'foo' - ] - ]); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + ]) + ])); $snippet = $this->snippetFromMethod(Transaction::class, 'executeUpdateBatch'); $snippet->addLocal('transaction', $this->transaction); @@ -229,11 +242,10 @@ public function testExecuteUpdateBatchError() public function testRead() { - $this->connection->streamingRead(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::type(ReadRequest::class), + Argument::type('array') + )->willReturn($this->resultGeneratorStream()); $snippet = $this->snippetFromMagicMethod(Transaction::class, 'read'); $snippet->addLocal('transaction', $this->transaction); @@ -255,26 +267,23 @@ public function testInsert() { $snippet = $this->snippetFromMethod(Transaction::class, 'insert'); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('insert', $mutations[0]); } - public function testInsertBatch() { $snippet = $this->snippetFromMethod(Transaction::class, 'insertBatch'); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('insert', $mutations[0]); } @@ -282,26 +291,23 @@ public function testUpdate() { $snippet = $this->snippetFromMethod(Transaction::class, 'update'); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('update', $mutations[0]); } - public function testUpdateBatch() { $snippet = $this->snippetFromMethod(Transaction::class, 'updateBatch'); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('update', $mutations[0]); } @@ -309,28 +315,23 @@ public function testInsertOrUpdate() { $snippet = $this->snippetFromMethod(Transaction::class, 'insertOrUpdate'); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('insertOrUpdate', $mutations[0]); } - public function testInsertOrUpdateBatch() { - $this->refreshOperation($this->transaction, $this->connection->reveal()); - $snippet = $this->snippetFromMethod(Transaction::class, 'insertOrUpdateBatch'); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('insertOrUpdate', $mutations[0]); } @@ -338,26 +339,23 @@ public function testReplace() { $snippet = $this->snippetFromMethod(Transaction::class, 'replace'); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('replace', $mutations[0]); } - public function testReplaceBatch() { $snippet = $this->snippetFromMethod(Transaction::class, 'replaceBatch'); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('replace', $mutations[0]); } @@ -366,21 +364,20 @@ public function testDelete() $snippet = $this->snippetFromMethod(Transaction::class, 'delete'); $snippet->addUse(KeySet::class); $snippet->addLocal('mutationGroup', $this->transaction); + $snippet->invoke(); - $this->refreshOperation($this->transaction, $this->connection->reveal()); - - $res = $snippet->invoke(); - - $mutations = $this->transaction->___getProperty('mutationData'); + $reflProp = new \ReflectionProperty($this->transaction, 'mutationData'); + $reflProp->setAccessible(true); + $mutations = $reflProp->getValue($this->transaction); $this->assertArrayHasKey('delete', $mutations[0]); } public function testRollback() { - $this->connection->rollback(Argument::any()) - ->shouldBeCalled(); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->rollback( + Argument::type(RollbackRequest::class), + Argument::type('array') + )->shouldBeCalledOnce(); $snippet = $this->snippetFromMethod(Transaction::class, 'rollback'); $snippet->addLocal('transaction', $this->transaction); @@ -390,13 +387,14 @@ public function testRollback() public function testCommit() { - $this->connection->commit(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString() - ]); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::type(CommitRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]) + ])); $snippet = $this->snippetFromMethod(Transaction::class, 'commit'); $snippet->addLocal('transaction', $this->transaction); @@ -407,20 +405,19 @@ public function testCommit() public function testGetCommitStats() { $expectedCommitStats = new CommitStats(['mutation_count' => 4]); - $this->connection->commit(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'commitTimestamp' => (new Timestamp(new \DateTime))->formatAsString(), - 'commitStats' => $expectedCommitStats, - ]); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->commit( + Argument::type(CommitRequest::class), + Argument::type('array') + )->willReturn(new CommitResponse([ + 'commit_timestamp' => new TimestampProto(['seconds' => time()]), + 'commit_stats' => $expectedCommitStats, + ])); $snippet = $this->snippetFromMethod(Transaction::class, 'getCommitStats'); $snippet->addLocal('transaction', $this->transaction); $res = $snippet->invoke('commitStats'); - $this->assertEquals($expectedCommitStats, $res->returnVal()); + $this->assertEquals(['mutationCount' => 4], $res->returnVal()); } public function testState() @@ -437,8 +434,6 @@ 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()); } diff --git a/Spanner/tests/Snippet/TransactionalReadMethodsTest.php b/Spanner/tests/Snippet/TransactionalReadMethodsTest.php index 5ad92609999c..1732679fb3ea 100644 --- a/Spanner/tests/Snippet/TransactionalReadMethodsTest.php +++ b/Spanner/tests/Snippet/TransactionalReadMethodsTest.php @@ -17,23 +17,26 @@ namespace Google\Cloud\Spanner\Tests\Snippet; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Snippet\SnippetTestCase; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\Batch\BatchSnapshot; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\Snapshot; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; -use Google\Cloud\Spanner\V1\Gapic\SpannerGapicClient; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\PartialResultSet; +use Google\Cloud\Spanner\V1\ReadRequest; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -49,9 +52,8 @@ class TransactionalReadMethodsTest extends SnippetTestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; - use StubCreationTrait; + use ResultGeneratorTrait; const PROJECT = 'my-awesome-project'; const DATABASE = 'my-database'; @@ -59,7 +61,9 @@ class TransactionalReadMethodsTest extends SnippetTestCase const TRANSACTION = 'my-transaction'; const SESSION = 'projects/my-awesome-project/instances/my-instance/databases/my-database/sessions/session-id'; - private $connection; + private $spannerClient; + private $databaseAdminClient; + private $serializer; private $session; private $operation; @@ -71,168 +75,152 @@ public function setUp(): void { parent::setUpBeforeClass(); - $this->connection = $this->getConnStub(); + $this->serializer = new Serializer(); $this->session = $this->prophesize(Session::class); $this->session->info() ->willReturn([ 'databaseName' => 'database' ]); - $this->operation = $this->prophesize(Operation::class); + $this->session->name() + ->willReturn('sessionName'); + $this->session->setExpiration(); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->operation = new Operation( + $this->spannerClient->reveal(), + $this->serializer + ); + $this->databaseAdminClient = $this->prophesize(DatabaseAdminClient::class); } - public function clientAndSnippetExecute() + public function clientAndSnippet() { return [ - ['database', $this->setupDatabase(), $this->snippetFromMethod(Database::class, 'execute')], - ['transaction', $this->setupTransaction(), $this->snippetFromMethod(Transaction::class, 'execute')], - ['transaction', $this->setupSnapshot(), $this->snippetFromMethod(Snapshot::class, 'execute')], - ['transaction', $this->setupBatch(), $this->snippetFromMethod(BatchSnapshot::class, 'execute')], + ['database', Database::class], + ['transaction', Transaction::class], + ['transaction', Snapshot::class], + ['transaction', BatchSnapshot::class], ]; } /** - * @dataProvider clientAndSnippetExecute + * @dataProvider clientAndSnippet */ - public function testExecute($localName, $client, $snippet) + public function testExecute($localName, $clientClass) { $this->checkAndSkipGrpcTests(); - $this->connection->executeStreamingSql(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'loginCount', - 'type' => [ - 'code' => Database::TYPE_INT64 - ] - ] - ] - ] - ], - 'values' => [0] + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([ + [ + 'name' => 'loginCount', + 'type' => Database::TYPE_INT64, + 'value' => 0 + ] ])); - $this->refreshOperation($client, $this->connection->reveal()); + $client = $this->createClientForClass($clientClass); + $snippet = $this->snippetFromMethod($clientClass, 'execute'); $snippet->addLocal($localName, $client); $res = $snippet->invoke('result'); $this->assertInstanceOf(Result::class, $res->returnVal()); } - public function clientAndSnippetExecuteParameterType() - { - return [ - ['database', $this->setupDatabase(), $this->snippetFromMethod(Database::class, 'execute', 1)], - ['transaction', $this->setupTransaction(), $this->snippetFromMethod(Transaction::class, 'execute', 1)], - ['transaction', $this->setupSnapshot(), $this->snippetFromMethod(Snapshot::class, 'execute', 1)], - ['transaction', $this->setupBatch(), $this->snippetFromMethod(BatchSnapshot::class, 'execute', 1)], - ]; - } - /** - * @dataProvider clientAndSnippetExecuteParameterType + * @dataProvider clientAndSnippet */ - public function testExecuteWithParameterType($localName, $client, $snippet) + public function testExecuteWithParameterType($localName, $clientClass) { $this->checkAndSkipGrpcTests(); - $this->connection->executeStreamingSql(Argument::that(function ($arg) { - if (!isset($arg['params'])) { - return false; - } - - if (!isset($arg['paramTypes'])) { - return false; - } - - if ($arg['paramTypes']['timestamp']['code'] !== Database::TYPE_TIMESTAMP) { - return false; - } - - return true; - }))->shouldBeCalled()->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'timestamp', - 'type' => [ - 'code' => Database::TYPE_TIMESTAMP - ] - ] - ] - ] - ], - 'values' => [null] - ])); - - $this->refreshOperation($client, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($args) { + $message = $this->serializer->encodeMessage($args); + $this->assertTrue(isset($message['params'])); + $this->assertTrue(isset($message['paramTypes'])); + $this->assertEquals( + $message['paramTypes']['timestamp']['code'], + Database::TYPE_TIMESTAMP + ); + return true; + }), + Argument::type('array') + )->willReturn( + $this->resultGeneratorStream([ + [ + 'name' => 'timestamp', + 'type' => Database::TYPE_TIMESTAMP, + 'value' => null, + ], + ]) + ); + $client = $this->createClientForClass($clientClass); + $snippet = $this->snippetFromMethod($clientClass, 'execute', 1); $snippet->addLocal($localName, $client); $res = $snippet->invoke('timestamp'); - $this->assertNull($res->returnVal()); - } - - public function clientAndSnippetExecuteEmptyArray() - { - return [ - ['database', $this->setupDatabase(), $this->snippetFromMethod(Database::class, 'execute', 2)], - ['transaction', $this->setupTransaction(), $this->snippetFromMethod(Transaction::class, 'execute', 2)], - ['transaction', $this->setupSnapshot(), $this->snippetFromMethod(Snapshot::class, 'execute', 2)], - ['transaction', $this->setupBatch(), $this->snippetFromMethod(BatchSnapshot::class, 'execute', 2)], - ]; + $this->assertEquals('', $res->returnVal()); } /** - * @dataProvider clientAndSnippetExecuteEmptyArray + * @dataProvider clientAndSnippet */ - public function testExecuteWithEmptyArray($localName, $client, $snippet) + public function testExecuteWithEmptyArray($localName, $clientClass) { $this->checkAndSkipGrpcTests(); - $this->connection->executeStreamingSql(Argument::that(function ($arg) { - if (!isset($arg['params'])) { - return false; - } - - if (!isset($arg['paramTypes'])) { - 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([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'numbers', - 'type' => [ - 'code' => Database::TYPE_ARRAY, - 'arrayElementType' => [ - 'code' => Database::TYPE_INT64 + $snippet = $this->snippetFromMethod($clientClass, 'execute', 2); + $client = $this->createClientForClass($clientClass); + + $partialResultSet = $this->serializer->decodeMessage( + new PartialResultSet(), + [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'numbers', + 'type' => [ + 'code' => Database::TYPE_ARRAY, + 'arrayElementType' => [ + 'code' => Database::TYPE_INT64 + ] ] ] ] ] + ], + 'values' => [ + ['listValue' => []] ] - ], - 'values' => [[]] - ])); - - $this->refreshOperation($client, $this->connection->reveal()); + ] + ); + + $this->spannerClient->executeStreamingSql( + Argument::that(function ($args) { + $message = $this->serializer->encodeMessage($args); + $this->assertTrue(isset($message['params'])); + $this->assertTrue(isset($message['paramTypes'])); + $this->assertEquals( + $message['paramTypes']['emptyArrayOfIntegers']['code'], + Database::TYPE_ARRAY + ); + $this->assertEquals( + $message['paramTypes']['emptyArrayOfIntegers']['arrayElementType']['code'], + Database::TYPE_INT64 + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream([$partialResultSet])); $snippet->addLocal($localName, $client); @@ -240,65 +228,50 @@ public function testExecuteWithEmptyArray($localName, $client, $snippet) $this->assertEmpty($res->returnVal()); } - public function clientAndSnippetExecuteStruct() - { - return [ - ['database', $this->setupDatabase(), $this->snippetFromMethod(Database::class, 'execute', 3)], - ['transaction', $this->setupTransaction(), $this->snippetFromMethod(Transaction::class, 'execute', 3)], - ['transaction', $this->setupSnapshot(), $this->snippetFromMethod(Snapshot::class, 'execute', 3)], - ['transaction', $this->setupBatch(), $this->snippetFromMethod(BatchSnapshot::class, 'execute', 3)], - ]; - } - /** - * @dataProvider clientAndSnippetExecuteStruct + * @dataProvider clientAndSnippet */ - public function testExecuteStruct($localName, $client, $snippet) + public function testExecuteStruct($localName, $clientClass) { $this->checkAndSkipGrpcTests(); - $fields = [ + $snippet = $this->snippetFromMethod($clientClass, 'execute', 3); + $client = $this->createClientForClass($clientClass); + + $rows = [ [ 'name' => 'firstName', - 'type' => [ - 'code' => Database::TYPE_STRING - ] + 'type' => Database::TYPE_STRING, + 'value' => 'John' ], [ 'name' => 'lastName', - 'type' => [ - 'code' => Database::TYPE_STRING - ] + 'type' => Database::TYPE_STRING, + 'value' => 'Testuser', ] ]; - $values = [ - 'John', - 'Testuser' - ]; - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', 'SELECT @userStruct.firstName, @userStruct.lastName'), - Argument::withEntry('params', [ - 'userStruct' => $values - ]), - Argument::withEntry('paramTypes', [ - 'userStruct' => [ - 'code' => Database::TYPE_STRUCT, - 'structType' => [ - 'fields' => $fields - ] - ] - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => $fields - ] - ], - 'values' => $values - ])); - - $this->refreshOperation($client, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($args) use ($rows) { + $message = $this->serializer->encodeMessage($args); + $this->assertEquals($message['sql'], 'SELECT @userStruct.firstName, @userStruct.lastName'); + $this->assertEquals( + $message['params']['userStruct'], + array_map(fn ($row) => $row['value'], $rows) + ); + $this->assertEquals( + $message['paramTypes']['userStruct']['structType']['fields'][0]['name'], + $rows[0]['name'] + ); + $this->assertEquals( + $message['paramTypes']['userStruct']['structType']['fields'][1]['name'], + $rows[1]['name'] + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->wilLReturn($this->resultGeneratorStream($rows)); $snippet->addLocal($localName, $client); @@ -306,70 +279,51 @@ public function testExecuteStruct($localName, $client, $snippet) $this->assertEquals('John Testuser', $res->returnVal()); } - public function clientAndSnippetExecuteDuplicateAndUnnamedFields() - { - return [ - ['database', $this->setupDatabase(), $this->snippetFromMethod(Database::class, 'execute', 4)], - ['transaction', $this->setupTransaction(), $this->snippetFromMethod(Transaction::class, 'execute', 4)], - ['transaction', $this->setupSnapshot(), $this->snippetFromMethod(Snapshot::class, 'execute', 4)], - ['transaction', $this->setupBatch(), $this->snippetFromMethod(BatchSnapshot::class, 'execute', 4)], - ]; - } - /** - * @dataProvider clientAndSnippetExecuteDuplicateAndUnnamedFields + * @dataProvider clientAndSnippet */ - public function testExecuteStructDuplicateAndUnnamedFields($localName, $client, $snippet) + public function testExecuteStructDuplicateAndUnnamedFields($localName, $clientClass) { $this->checkAndSkipGrpcTests(); - $fields = [ + $snippet = $this->snippetFromMethod($clientClass, 'execute', 4); + $client = $this->createClientForClass($clientClass); + + $rows = [ [ 'name' => 'foo', - 'type' => [ - 'code' => Database::TYPE_STRING - ] + 'type' => Database::TYPE_STRING, + 'value' => 'bar' ], [ 'name' => 'foo', - 'type' => [ - 'code' => Database::TYPE_INT64 - ] + 'type' => Database::TYPE_INT64, + 'value' => 2 ], [ - 'type' => [ - 'code' => Database::TYPE_STRING - ] + 'name' => '', + 'type' => Database::TYPE_STRING, + 'value' => 'this field is unnamed' ] ]; - $values = [ - 'bar', - 2, - 'this field is unnamed' - ]; - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', 'SELECT * FROM UNNEST(ARRAY(SELECT @structParam))'), - Argument::withEntry('params', [ - 'structParam' => $values - ]), - Argument::withEntry('paramTypes', [ - 'structParam' => [ - 'code' => Database::TYPE_STRUCT, - 'structType' => [ - 'fields' => $fields - ] - ] - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => $fields - ] - ], - 'values' => $values - ])); - - $this->refreshOperation($client, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($args) use ($rows) { + $message = $this->serializer->encodeMessage($args); + $this->assertEquals( + $message['sql'], + 'SELECT * FROM UNNEST(ARRAY(SELECT @structParam))' + ); + $this->assertEquals( + $message['params']['structParam'], + array_map(fn ($v) => $v['value'], $rows) + ); + $this->assertEquals( + $message['paramTypes']['structParam']['structType']['fields'][0]['name'], + $rows[0]['name'] + ); + return true; + }), + Argument::type('array') + )->willReturn($this->resultGeneratorStream($rows)); $snippet->addLocal($localName, $client); @@ -379,42 +333,26 @@ public function testExecuteStructDuplicateAndUnnamedFields($localName, $client, $this->assertEquals('2: this field is unnamed', $res[2]); } - public function clientAndSnippetRead() - { - return [ - ['database', $this->setupDatabase(), $this->snippetFromMethod(Database::class, 'read')], - ['transaction', $this->setupTransaction(), $this->snippetFromMethod(Transaction::class, 'read')], - ['transaction', $this->setupSnapshot(), $this->snippetFromMethod(Snapshot::class, 'read')], - ['transaction', $this->setupBatch(), $this->snippetFromMethod(BatchSnapshot::class, 'read')], - ]; - } - /** - * @dataProvider clientAndSnippetRead + * @dataProvider clientAndSnippet */ - public function testRead($localName, $client, $snippet) + public function testRead($localName, $clientClass) { $this->checkAndSkipGrpcTests(); - $this->connection->streamingRead(Argument::any()) - ->shouldBeCalled() - ->willReturn($this->resultGenerator([ - 'metadata' => [ - 'rowType' => [ - 'fields' => [ - [ - 'name' => 'loginCount', - 'type' => [ - 'code' => Database::TYPE_INT64 - ] - ] - ] - ] - ], - 'rows' => [0] - ])); + $snippet = $this->snippetFromMethod($clientClass, 'read'); + $client = $this->createClientForClass($clientClass); - $this->refreshOperation($client, $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::type(ReadRequest::class), + Argument::type('array') + )->willReturn($this->resultGeneratorStream([ + [ + 'name' => 'loginCount', + 'type' => Database::TYPE_INT64, + 'value' => 0, + ] + ])); $snippet->addLocal($localName, $client); @@ -424,10 +362,9 @@ public function testRead($localName, $client, $snippet) private function setupDatabase() { - $this->setUp(); - $instance = $this->prophesize(Instance::class); $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); + $instance->directedReadOptions()->willReturn([]); $sessionPool = $this->prophesize(SessionPoolInterface::class); $sessionPool->acquire(Argument::any()) @@ -435,67 +372,68 @@ private function setupDatabase() $sessionPool->setDatabase(Argument::any()) ->willReturn(null); - return \Google\Cloud\Core\Testing\TestHelpers::stub(Database::class, [ - $this->connection->reveal(), + return new Database( + $this->spannerClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, $instance->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], self::PROJECT, self::DATABASE, - $sessionPool->reveal() - ], ['operation']); + ['sessionPool' => $sessionPool->reveal()] + ); } private function setupTransaction() { - $this->setUp(); - - return \Google\Cloud\Core\Testing\TestHelpers::stub(Transaction::class, [ - $this->operation->reveal(), + return new Transaction( + $this->operation, $this->session->reveal(), self::TRANSACTION - ], ['operation']); + ); } private function setupSnapshot() { - $this->setUp(); - - return \Google\Cloud\Core\Testing\TestHelpers::stub(Snapshot::class, [ - $this->operation->reveal(), + return new Snapshot( + $this->operation, $this->session->reveal(), [ 'id' => self::TRANSACTION, - 'readTimestamp' => new Timestamp(new \DateTime) + 'readTimestamp' => new Timestamp(new \DateTime()) ] - ], ['operation']); + ); } private function setupBatch() { - $sessData = SpannerGapicClient::parseName(self::SESSION, 'session'); + $sessData = SpannerClient::parseName(self::SESSION, 'session'); $this->session->name()->willReturn(self::SESSION); $this->session->info()->willReturn($sessData + [ 'name' => self::SESSION, - 'databaseName' => SpannerGapicClient::databaseName( + 'databaseName' => SpannerClient::databaseName( self::PROJECT, self::INSTANCE, self::DATABASE ) ]); - return \Google\Cloud\Core\Testing\TestHelpers::stub(BatchSnapshot::class, [ - new Operation($this->connection->reveal(), false), + return new BatchSnapshot( + new Operation($this->spannerClient->reveal(), $this->serializer), $this->session->reveal(), [ 'id' => self::TRANSACTION, 'readTimestamp' => new Timestamp(\DateTime::createFromFormat('U', (string) time())) ] - ], ['operation', 'session']); + ); } - private function resultGenerator(array $data) + private function createClientForClass(string $clientClass) { - yield $data; + return match ($clientClass) { + Database::class => $this->setupDatabase(), + Transaction::class => $this->setupTransaction(), + Snapshot::class => $this->setupSnapshot(), + BatchSnapshot::class => $this->setupBatch(), + }; } } diff --git a/Spanner/tests/StubCreationTrait.php b/Spanner/tests/StubCreationTrait.php deleted file mode 100644 index 5c0d328171f6..000000000000 --- a/Spanner/tests/StubCreationTrait.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php -/** - * Copyright 2019 Google LLC - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests; - -use Google\Cloud\Spanner\Connection\ConnectionInterface; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; - -/** - * Creates stubs with required global expectations. - */ -trait StubCreationTrait -{ - use ProphecyTrait; - - private function getConnStub() - { - $c = $this->prophesize(ConnectionInterface::class); - $c->deleteSession(Argument::any())->willReturn([]); - - return $c; - } -} diff --git a/Spanner/tests/System/AdminTest.php b/Spanner/tests/System/AdminTest.php index 1fdb6a871c6e..9d6c93926f87 100644 --- a/Spanner/tests/System/AdminTest.php +++ b/Spanner/tests/System/AdminTest.php @@ -19,10 +19,9 @@ use Google\Cloud\Core\Exception\FailedPreconditionException; use Google\Cloud\Core\LongRunning\LongRunningOperation; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig\Type; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; @@ -33,6 +32,14 @@ */ class AdminTest extends SpannerTestCase { + /** + * @beforeClass + */ + public static function setUpTestFixtures(): void + { + self::setUpTestDatabase(); + } + /** * covers 121 */ @@ -79,7 +86,11 @@ public function testInstance() 'nodeCount' => 0, 'processingUnits' => 0, 'state' => Instance::STATE_READY, - 'config' => '' + 'config' => '', + 'replicaComputeCapacity' => [], + 'edition' => 0, + 'defaultBackupScheduleType' => 0, + 'instanceType' => 0, ]; $info = $instance->reload(['fieldMask' => $requestedFieldNames]); $this->assertEquals($expectedInfo, $info); @@ -96,7 +107,8 @@ public function testDatabase() $op = $instance->createDatabase($dbName); $this->assertInstanceOf(LongRunningOperation::class, $op); - $db = $op->pollUntilComplete(); + $op->pollUntilComplete(); + $db = $op->result(); $this->assertInstanceOf(Database::class, $db); self::$deletionQueue->add(function () use ($db) { @@ -114,7 +126,7 @@ public function testDatabase() $expectedDatabaseDialect = DatabaseDialect::GOOGLE_STANDARD_SQL; // TODO: Remove this, when the emulator supports PGSQL - if ((bool) getenv("SPANNER_EMULATOR_HOST")) { + if ((bool) getenv('SPANNER_EMULATOR_HOST')) { $expectedDatabaseDialect = DatabaseDialect::DATABASE_DIALECT_UNSPECIFIED; } @@ -122,7 +134,7 @@ public function testDatabase() $stmt = "CREATE TABLE Ids (\n" . " id INT64 NOT NULL,\n" . - ") PRIMARY KEY(id)"; + ') PRIMARY KEY(id)'; $op = $db->updateDdl($stmt); $op->pollUntilComplete(); @@ -139,7 +151,8 @@ public function testDatabaseDropProtection() $op = $instance->createDatabase($dbName); $this->assertInstanceOf(LongRunningOperation::class, $op); - $db = $op->pollUntilComplete(); + $op->pollUntilComplete(); + $db = $op->result(); $this->assertInstanceOf(Database::class, $db); $info = $db->reload(); diff --git a/Spanner/tests/System/BackupTest.php b/Spanner/tests/System/BackupTest.php index 5d8633c6e7e0..2f2e716ca060 100644 --- a/Spanner/tests/System/BackupTest.php +++ b/Spanner/tests/System/BackupTest.php @@ -17,14 +17,13 @@ namespace Google\Cloud\Spanner\Tests\System; +use Google\ApiCore\OperationResponse; use Google\Cloud\Core\Exception\BadRequestException; use Google\Cloud\Core\Exception\ConflictException; -use Google\Cloud\Core\LongRunning\LongRunningOperation; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Database\V1\CreateBackupEncryptionConfig; -use Google\Cloud\Spanner\Admin\Database\V1\RestoreDatabaseEncryptionConfig; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Admin\Database\V1\EncryptionConfig; use Google\Cloud\Spanner\Admin\Database\V1\EncryptionInfo\Type; +use Google\Cloud\Spanner\Admin\Database\V1\RestoreDatabaseEncryptionConfig; use Google\Cloud\Spanner\Backup; use Google\Cloud\Spanner\Date; @@ -40,7 +39,6 @@ class BackupTest extends SpannerTestCase protected static $copyBackupId; protected static $backupOperationName; protected static $restoreOperationName; - protected static $createDbOperationName; protected static $createTime1; protected static $createTime2; @@ -58,120 +56,112 @@ public static function setUpTestFixtures(): void { self::skipEmulatorTests(); - parent::setUpTestFixtures(); + self::setUpTestDatabase(); if (self::$hasSetUp) { return; } self::$project = self::parseName(self::$instance->name(), 'project'); - self::$dbName1 = uniqid(self::TESTING_PREFIX); - $op = self::$instance->createDatabase(self::$dbName1); - self::$createDbOperationName = $op->name(); - $op->pollUntilComplete(); - - $db1 = self::getDatabaseInstance(self::$dbName1); - - self::$deletionQueue->add(function () use ($db1) { - $db1->drop(); - }); - - $db1->updateDdl( - 'CREATE TABLE '. self::TEST_TABLE_NAME .' ( - id INT64 NOT NULL, - name STRING(MAX) NOT NULL, - birthday DATE NOT NULL - ) PRIMARY KEY (id)' - )->pollUntilComplete(); + if (!self::$dbName1 = getenv('GOOGLE_CLOUD_SPANNER_TEST_BACKUP_DATABASE_1')) { + self::$dbName1 = uniqid(self::TESTING_PREFIX); + self::$deletionQueue->add(function () { + self::getDatabaseInstance(self::$dbName1)->drop(); + }); + } - self::$dbName2 = uniqid(self::TESTING_PREFIX); - $op = self::$instance->createDatabase(self::$dbName2); - $op->pollUntilComplete(); + if (!self::$dbName2 = getenv('GOOGLE_CLOUD_SPANNER_TEST_BACKUP_DATABASE_2')) { + self::$dbName2 = uniqid(self::TESTING_PREFIX); + self::$deletionQueue->add(function () { + self::getDatabaseInstance(self::$dbName2)->drop(); + }); + } + $db1 = self::getDatabaseInstance(self::$dbName1); $db2 = self::getDatabaseInstance(self::$dbName2); - self::$deletionQueue->add(function () use ($db2) { - $db2->drop(); - }); - - $db2->updateDdl( - 'CREATE TABLE '. self::TEST_TABLE_NAME .' ( - id INT64 NOT NULL, - name STRING(MAX) NOT NULL, - birthday DATE NOT NULL - ) PRIMARY KEY (id)' - )->pollUntilComplete(); + if (!$db1->exists()) { + $op = self::$instance->createDatabase(self::$dbName1); + $op->pollUntilComplete(); + $db1->updateDdl( + 'CREATE TABLE ' . self::TEST_TABLE_NAME . ' ( + id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + birthday DATE NOT NULL + ) PRIMARY KEY (id)' + )->pollUntilComplete(); + self::insertData(5, self::$dbName1); + } - self::insertData(5, self::$dbName1); - self::insertData(10, self::$dbName2); + if (!$db2->exists()) { + $op = self::$instance->createDatabase(self::$dbName2); + $op->pollUntilComplete(); + + $db2->updateDdl( + 'CREATE TABLE ' . self::TEST_TABLE_NAME . ' ( + id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + birthday DATE NOT NULL + ) PRIMARY KEY (id)' + )->pollUntilComplete(); + self::insertData(10, self::$dbName2); + } self::$backupId1 = uniqid(self::BACKUP_PREFIX); - self::$backupId2 = uniqid("users-"); - self::$copyBackupId = uniqid("copy-"); + self::$backupId2 = uniqid('users-'); + self::$copyBackupId = uniqid('copy-'); self::$hasSetUp = true; } - public function testListAllInstances() - { - $allInstances = self::$client->instances(); - - foreach ($allInstances as $i) { - print(PHP_EOL); - print_r($i->name()); - } - } - - public function testCreateBackup() - { - $expireTime = new \DateTime('+7 hours'); - $versionTime = new \DateTime('-5 seconds'); - $encryptionConfig = [ - 'encryptionType' => CreateBackupEncryptionConfig\EncryptionType::GOOGLE_DEFAULT_ENCRYPTION, - ]; - - $backup = self::$instance->backup(self::$backupId1); - $db1 = self::getDatabaseInstance(self::$dbName1); - - self::$createTime1 = gmdate('"Y-m-d\TH:i:s\Z"'); - $op = $backup->create(self::$dbName1, $expireTime, [ - 'versionTime' => $versionTime, - 'encryptionConfig' => $encryptionConfig, - ]); - self::$backupOperationName = $op->name(); - - $metadata = null; - foreach (self::$instance->backupOperations() as $listItem) { - if ($listItem->name() == $op->name()) { - $metadata = $listItem->info()['metadata']; - break; - } - } - - $op->pollUntilComplete(); - - self::$deletionQueue->add(function () use ($backup) { - $backup->delete(); - }); - - $this->assertTrue($backup->exists()); - $this->assertInstanceOf(Backup::class, $backup); - $this->assertEquals(self::$backupId1, DatabaseAdminClient::parseName($backup->info()['name'])['backup']); - $this->assertEquals(self::$dbName1, DatabaseAdminClient::parseName($backup->info()['database'])['database']); - $this->assertEquals($expireTime->format('Y-m-d\TH:i:s.u\Z'), $backup->info()['expireTime']); - $this->assertTrue(is_string($backup->info()['createTime'])); - $this->assertEquals(Backup::STATE_READY, $backup->state()); - $this->assertTrue($backup->info()['sizeBytes'] > 0); - // earliestVersionTime deviates from backup's versionTime by a couple of minutes - $expectedDateTime = \DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $db1->info()['earliestVersionTime']); - $actualDateTime = \DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $backup->info()['versionTime']); - $this->assertEqualsWithDelta($expectedDateTime->getTimestamp(), $actualDateTime->getTimestamp(), 300); - $this->assertEquals(Type::GOOGLE_DEFAULT_ENCRYPTION, $backup->info()['encryptionInfo']['encryptionType']); - - $this->assertNotNull($metadata); - $this->assertArrayHasKey('progress', $metadata); - $this->assertArrayHasKey('progressPercent', $metadata['progress']); - $this->assertArrayHasKey('startTime', $metadata['progress']); - } + // public function testCreateBackup() + // { + // $expireTime = new \DateTime('+7 hours'); + // $encryptionConfig = [ + // 'encryptionType' => CreateBackupEncryptionConfig\EncryptionType::GOOGLE_DEFAULT_ENCRYPTION, + // ]; + + // $backup = self::$instance->backup(self::$backupId1); + // $db1 = self::getDatabaseInstance(self::$dbName1); + + // self::$createTime1 = gmdate('"Y-m-d\TH:i:s\Z"'); + // $op = $backup->create(self::$dbName1, $expireTime, [ + // 'encryptionConfig' => $encryptionConfig, + // ]); + // self::$backupOperationName = $op->name(); + + // $metadata = null; + // foreach (self::$instance->backupOperations() as $listItem) { + // if ($listItem->name() == $op->name()) { + // $metadata = $listItem->info()['metadata']; + // break; + // } + // } + + // $op->pollUntilComplete(); + + // self::$deletionQueue->add(function () use ($backup) { + // $backup->delete(); + // }); + + // $this->assertTrue($backup->exists()); + // $this->assertInstanceOf(Backup::class, $backup); + // $this->assertEquals(self::$backupId1, DatabaseAdminClient::parseName($backup->info()['name'])['backup']); + // $this->assertEquals(self::$dbName1, DatabaseAdminClient::parseName($backup->info()['database'])['database']); + // $this->assertEquals($expireTime->format('Y-m-d\TH:i:s.u\Z'), $backup->info()['expireTime']); + // $this->assertTrue(is_string($backup->info()['createTime'])); + // $this->assertEquals(Backup::STATE_READY, $backup->state()); + // $this->assertTrue($backup->info()['sizeBytes'] > 0); + // // earliestVersionTime deviates from backup's versionTime by a couple of minutes + // $expectedDateTime = \DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $db1->info()['earliestVersionTime']); + // $actualDateTime = \DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $backup->info()['versionTime']); + // $this->assertEqualsWithDelta($expectedDateTime->getTimestamp(), $actualDateTime->getTimestamp(), 300); + // $this->assertEquals(Type::GOOGLE_DEFAULT_ENCRYPTION, $backup->info()['encryptionInfo']['encryptionType']); + + // $this->assertNotNull($metadata); + // $this->assertArrayHasKey('progress', $metadata); + // $this->assertArrayHasKey('progressPercent', $metadata['progress']); + // $this->assertArrayHasKey('startTime', $metadata['progress']); + // } public function testCreateBackupRequestFailed() { @@ -200,7 +190,7 @@ public function testCreateBackupInvalidArgument() $e = null; try { $backup->create(self::$dbName1, $expireTime, [ - 'versionTime' => "invalidType", + 'versionTime' => 'invalidType', ]); } catch (\InvalidArgumentException $e) { } @@ -220,6 +210,9 @@ public function testCreateBackupInvalidArgument() $this->assertFalse($backup->exists()); } + /** + * @depends testCreateBackup + */ public function testCancelBackupOperation() { $expireTime = new \DateTime('+7 hours'); @@ -335,6 +328,9 @@ public function testUpdateExpirationTimeFailed() $this->assertEquals($currentExpireTime, $backup->info()['expireTime']); } + /** + * @depends testCreateBackup + */ public function testListAllBackups() { $allBackups = iterator_to_array(self::$instance->backups(), false); @@ -347,6 +343,9 @@ public function testListAllBackups() $this->assertContainsOnlyInstancesOf(Backup::class, $allBackups); } + /** + * @depends testCreateBackup + */ public function testListAllBackupsContainsName() { $backups = iterator_to_array(self::$instance->backups(['filter' => 'name:' . self::$backupId1])); @@ -354,9 +353,12 @@ public function testListAllBackupsContainsName() $this->assertEquals(self::$backupId1, DatabaseAdminClient::parseName($backups[0]->info()['name'])['backup']); } + /** + * @depends testCreateBackup + */ public function testListAllBackupsReady() { - $backups = iterator_to_array(self::$instance->backups(['filter'=>'state:READY'])); + $backups = iterator_to_array(self::$instance->backups(['filter' => 'state:READY'])); $backupNames = []; foreach ($backups as $b) { @@ -366,6 +368,9 @@ public function testListAllBackupsReady() $this->assertTrue(in_array(self::fullyQualifiedBackupName(self::$backupId1), $backupNames)); } + /** + * @depends testCreateBackup + */ public function testListAllBackupsOfDatabase() { $database = self::$instance->database(self::$dbName1); @@ -378,26 +383,31 @@ public function testListAllBackupsOfDatabase() } } + /** + * @depends testCreateBackup + */ public function testListAllBackupsCreatedAfterTimestamp() { - $filter = sprintf("create_time >= %s", self::$createTime2); + $filter = sprintf('create_time >= %s', self::$createTime1); - $backups = iterator_to_array(self::$instance->backups(['filter'=>$filter])); + $backups = iterator_to_array(self::$instance->backups(['filter' => $filter])); $backupNames = []; foreach ($backups as $b) { $backupNames[] = $b->name(); } $this->assertTrue(count($backupNames) > 0); - $this->assertFalse(in_array(self::fullyQualifiedBackupName(self::$backupId1), $backupNames)); - $this->assertTrue(in_array(self::fullyQualifiedBackupName(self::$backupId2), $backupNames)); + $this->assertTrue(in_array(self::fullyQualifiedBackupName(self::$backupId1), $backupNames)); } + /** + * @depends testCreateBackup + */ public function testListAllBackupsExpireBeforeTimestamp() { - $filter = "expire_time < " . gmdate('"Y-m-d\TH:i:s\Z"', strtotime('+9 hours')); + $filter = 'expire_time < ' . gmdate('"Y-m-d\TH:i:s\Z"', strtotime('+9 hours')); - $backups = iterator_to_array(self::$instance->backups(['filter'=>$filter])); + $backups = iterator_to_array(self::$instance->backups(['filter' => $filter])); $backupNames = []; foreach ($backups as $b) { @@ -415,7 +425,7 @@ public function testListAllBackupsWithSizeGreaterThanSomeBytes() { $backup = self::$instance->backup(self::$backupId1); $size = $backup->info()['sizeBytes']; - $filter = "size_bytes > " . $size; + $filter = 'size_bytes > ' . $size; $backups = iterator_to_array(self::$instance->backups(['filter' => $filter])); @@ -429,6 +439,9 @@ public function testListAllBackupsWithSizeGreaterThanSomeBytes() $this->assertTrue(in_array(self::fullyQualifiedBackupName(self::$backupId2), $backupNames)); } + /** + * @depends testCancelBackupOperation + */ public function testPagination() { $backupsfirstPage = self::$instance->backups(['pageSize' => 1]); @@ -445,6 +458,9 @@ public function testPagination() $this->assertEquals(2, count($backupsPageSizeTwo)); } + /** + * @depends testRestoreToNewDatabase + */ public function testListAllBackupOperations() { $backupOps = iterator_to_array($this::$instance->backupOperations()); @@ -454,7 +470,7 @@ public function testListAllBackupOperations() }, $backupOps); $this->assertTrue(count($backupOps) > 0); - $this->assertContainsOnlyInstancesOf(LongRunningOperation::class, $backupOps); + $this->assertContainsOnlyInstancesOf(OperationResponse::class, $backupOps); $this->assertTrue(in_array(self::$backupOperationName, $backupOpsNames)); } @@ -477,7 +493,7 @@ public function testDeleteBackup() public function testDeleteNonExistantBackup() { - $backup = self::$instance->backup("does_not_exis"); + $backup = self::$instance->backup('does_not_exis'); $this->assertFalse($backup->exists()); @@ -557,6 +573,9 @@ public function testRestoreToNewDatabase() $this->assertArrayHasKey('startTime', $metadata['progress']); } + /** + * @depends testRestoreToNewDatabase + */ public function testRestoreAppearsInListDatabaseOperations() { $databaseOps = iterator_to_array($this::$instance->databaseOperations()); @@ -565,10 +584,13 @@ public function testRestoreAppearsInListDatabaseOperations() }, $databaseOps); $this->assertTrue(count($databaseOps) > 0); - $this->assertContainsOnlyInstancesOf(LongRunningOperation::class, $databaseOps); + $this->assertContainsOnlyInstancesOf(OperationResponse::class, $databaseOps); $this->assertTrue(in_array(self::$restoreOperationName, $databaseOpsNames)); } + /** + * @depends testCreateBackup + */ public function testRestoreBackupToAnExistingDatabase() { $existingDb = self::$instance->database(self::$dbName2); @@ -603,7 +625,7 @@ private static function generateRows($number) { $rows = []; - for ($id=1; $id <= $number; $id++) { + for ($id = 1; $id <= $number; $id++) { $rows[] = self::generateRow($id, uniqid(self::TESTING_PREFIX), new Date(new \DateTime())); } return $rows; diff --git a/Spanner/tests/System/BatchTest.php b/Spanner/tests/System/BatchTest.php index 2d90a07e6c08..36491f6881e3 100644 --- a/Spanner/tests/System/BatchTest.php +++ b/Spanner/tests/System/BatchTest.php @@ -17,12 +17,12 @@ namespace Google\Cloud\Spanner\Tests\System; +use Google\Cloud\Core\Exception\ServiceException; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; use Google\Cloud\Spanner\Batch\BatchClient; use Google\Cloud\Spanner\Batch\BatchSnapshot; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Core\Exception\ServiceException; /** * @group spanner @@ -43,7 +43,7 @@ public static function setUpTestFixtures(): void if (self::$isSetup) { return; } - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$tableName = uniqid(self::TESTING_PREFIX); @@ -84,7 +84,7 @@ public static function setUpTestFixtures(): void private static function seedTable() { - $decades = [1950,1960,1970,1980,1990,2000]; + $decades = [1950, 1960, 1970, 1980, 1990, 2000]; for ($i = 0; $i < 250; $i++) { self::$database->insert(self::$tableName, [ 'id' => self::randId(), @@ -121,7 +121,6 @@ public function testBatch() $partitions = $snapshot->partitionQuery($query, ['parameters' => $parameters]); $this->assertEquals(count($resultSet), $this->executePartitions($batch, $snapshot, $partitions)); - // ($table, KeySet $keySet, array $columns, array $options = []) $keySet = new KeySet([ 'ranges' => [ new KeyRange([ @@ -182,60 +181,6 @@ public function testBatchWithDbRole($dbRole, $expected) $snapshot->close(); } - public function testBatchWithDataBoostEnabled() - { - // Emulator does not support dataBoostEnabled - $this->skipEmulatorTests(); - - $query = 'SELECT - id, - decade - FROM ' . self::$tableName . ' - WHERE - decade > @earlyBound - AND - decade < @lateBound'; - - $parameters = [ - 'earlyBound' => 1960, - 'lateBound' => 1980 - ]; - - $resultSet = iterator_to_array(self::$database->execute($query, ['parameters' => $parameters])); - - $batch = self::$client->batch(self::INSTANCE_NAME, self::$dbName); - $string = $batch->snapshot()->serialize(); - - $snapshot = $batch->snapshotFromString($string); - - $partitions = $snapshot->partitionQuery($query, [ - 'parameters' => $parameters, - 'dataBoostEnabled' => true - ]); - $this->assertEquals(count($resultSet), $this->executePartitions($batch, $snapshot, $partitions)); - - $keySet = new KeySet([ - 'ranges' => [ - new KeyRange([ - 'start' => $parameters['earlyBound'], - 'startType' => KeyRange::TYPE_OPEN, - 'end' => $parameters['lateBound'], - 'endType' => KeyRange::TYPE_OPEN - ]) - ] - ]); - - $partitions = $snapshot->partitionRead( - self::$tableName, - $keySet, - ['id', 'decade'], - ['dataBoostEnabled' => true] - ); - $this->assertEquals(count($resultSet), $this->executePartitions($batch, $snapshot, $partitions)); - - $snapshot->close(); - } - private function executePartitions(BatchClient $client, BatchSnapshot $snapshot, array $partitions) { $partitionResultSet = []; diff --git a/Spanner/tests/System/BatchWriteTest.php b/Spanner/tests/System/BatchWriteTest.php index cbcf38031985..2bc30b34bd60 100644 --- a/Spanner/tests/System/BatchWriteTest.php +++ b/Spanner/tests/System/BatchWriteTest.php @@ -31,7 +31,7 @@ class BatchWriteTest extends SpannerTestCase public static function setUpTestFixtures(): void { self::skipEmulatorTests(); - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$database->updateDdlBatch([ 'CREATE TABLE Singers ( @@ -53,16 +53,16 @@ public function testBatchWrite() $mutationGroups = []; $mutationGroups[] = self::$database->mutationGroup() ->insertOrUpdate( - "Singers", + 'Singers', ['SingerId' => 16, 'FirstName' => 'Scarlet', 'LastName' => 'Terry'] ); $mutationGroups[] = self::$database->mutationGroup() ->insertOrUpdate( - "Singers", + 'Singers', ['SingerId' => 17, 'FirstName' => 'Marc', 'LastName' => 'Kristen'] )->insertOrUpdate( - "Albums", + 'Albums', ['AlbumId' => 1, 'SingerId' => 17, 'AlbumTitle' => 'Total Junk'] ); diff --git a/Spanner/tests/System/GeneratedAdminEmulatorTest.php b/Spanner/tests/System/GeneratedAdminEmulatorTest.php index b31d5f409ac0..cef39e7faef7 100644 --- a/Spanner/tests/System/GeneratedAdminEmulatorTest.php +++ b/Spanner/tests/System/GeneratedAdminEmulatorTest.php @@ -42,7 +42,7 @@ public static function setUpTestFixtures(): void public function testAdminClientEmulatorSupport() { - if (!getenv("SPANNER_EMULATOR_HOST")) { + if (!getenv('SPANNER_EMULATOR_HOST')) { self::markTestSkipped('This test is required to run only in the emulator.'); } diff --git a/Spanner/tests/System/LargeReadTest.php b/Spanner/tests/System/LargeReadTest.php index e491aca6e8f4..d54583db0822 100644 --- a/Spanner/tests/System/LargeReadTest.php +++ b/Spanner/tests/System/LargeReadTest.php @@ -31,7 +31,7 @@ class LargeReadTest extends SpannerTestCase //@codingStandardsIgnoreStart private static $data = [ - 'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z' + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' ]; //@codingStandardsIgnoreEnd @@ -43,7 +43,7 @@ class LargeReadTest extends SpannerTestCase */ public static function setUpTestFixtures(): void { - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$tableName = uniqid(self::TESTING_PREFIX); @@ -80,7 +80,7 @@ private static function seedTable() 'bytesArrayColumn' => self::randomArrayOfBytes($str), ]; - for ($i=0; $i < 10; $i++) { + for ($i = 0; $i < 10; $i++) { self::$database->insert(self::$tableName, self::$row + ['id' => self::randId()], [ 'timeoutMillis' => 50000 ]); @@ -143,7 +143,7 @@ private static function randomBytes(&$str) private static function randomArrayOfStrings() { $res = []; - for ($i=0; $i <= rand(1, 4); $i++) { + for ($i = 0; $i <= rand(1, 4); $i++) { $res[] = self::randomString(); } @@ -153,7 +153,7 @@ private static function randomArrayOfStrings() private static function randomArrayOfBytes(&$str) { $res = []; - for ($i=0; $i <= rand(1, 4); $i++) { + for ($i = 0; $i <= rand(1, 4); $i++) { $res[] = self::randomBytes($str); } diff --git a/Spanner/tests/System/OperationsTest.php b/Spanner/tests/System/OperationsTest.php index 50e88cb6fb80..d960482ef3e2 100644 --- a/Spanner/tests/System/OperationsTest.php +++ b/Spanner/tests/System/OperationsTest.php @@ -17,9 +17,9 @@ namespace Google\Cloud\Spanner\Tests\System; +use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\Timestamp; -use Google\Cloud\Core\Exception\ServiceException; /** * @group spanner @@ -47,7 +47,7 @@ public static function setUpTestFixtures(): void self::$name1 = uniqid(self::TESTING_PREFIX); self::$name2 = uniqid(self::TESTING_PREFIX); - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$database->insert(self::TEST_TABLE_NAME, [ 'id' => self::$id1, @@ -147,7 +147,7 @@ public function testEmptyRead() $keySet = self::$client->keySet(['keys' => [99999]]); - $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id','name']); + $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id', 'name']); $this->assertEmpty(iterator_to_array($res->rows())); } @@ -157,7 +157,7 @@ public function testEmptyReadOnIndex() $keySet = self::$client->keySet(['keys' => [99999]]); - $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id','name'], [ + $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id', 'name'], [ 'index' => self::TEST_INDEX_NAME ]); @@ -185,7 +185,7 @@ public function testReadNonExistentSingleKey() 'keys' => [99999] ]); - $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id','name']); + $res = $db->read(self::TEST_TABLE_NAME, $keySet, ['id', 'name']); $this->assertEmpty(iterator_to_array($res->rows())); } diff --git a/Spanner/tests/System/PartitionedDmlTest.php b/Spanner/tests/System/PartitionedDmlTest.php index 50f4ed7ded02..7f512c884b1f 100644 --- a/Spanner/tests/System/PartitionedDmlTest.php +++ b/Spanner/tests/System/PartitionedDmlTest.php @@ -17,8 +17,6 @@ namespace Google\Cloud\Spanner\Tests\System; -use Google\Cloud\Spanner\Tests\System\SpannerTestCase; - /** * @group spanner * @group spanner-pdml @@ -27,6 +25,14 @@ class PartitionedDmlTest extends SpannerTestCase { const PDML_TABLE = 'partitionedDml'; + /** + * @beforeClass + */ + public static function setUpTestFixtures(): void + { + self::setUpTestDatabase(); + } + public function testPdml() { $db = self::$database; diff --git a/Spanner/tests/System/PgBatchTest.php b/Spanner/tests/System/PgBatchTest.php index 082af61e5f4c..329517b40537 100644 --- a/Spanner/tests/System/PgBatchTest.php +++ b/Spanner/tests/System/PgBatchTest.php @@ -17,10 +17,10 @@ namespace Google\Cloud\Spanner\Tests\System; +use Google\Cloud\Core\Exception\ServiceException; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; use Google\Cloud\Spanner\Batch\BatchClient; use Google\Cloud\Spanner\Batch\BatchSnapshot; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; -use Google\Cloud\Core\Exception\ServiceException; /** * @group spanner @@ -42,7 +42,7 @@ public static function setUpTestFixtures(): void if (self::$isSetup) { return; } - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$tableName = uniqid(self::TESTING_PREFIX); @@ -140,7 +140,7 @@ private function executePartitions(BatchClient $client, BatchSnapshot $snapshot, private static function seedTable() { - $decades = [1950,1960,1970,1980,1990,2000]; + $decades = [1950, 1960, 1970, 1980, 1990, 2000]; for ($i = 0; $i < 250; $i++) { self::$database->insert(self::$tableName, [ 'id' => self::randId(), diff --git a/Spanner/tests/System/PgBatchWriteTest.php b/Spanner/tests/System/PgBatchWriteTest.php index ba7941772145..69bbe39a7fcb 100644 --- a/Spanner/tests/System/PgBatchWriteTest.php +++ b/Spanner/tests/System/PgBatchWriteTest.php @@ -34,7 +34,7 @@ public static function setUpTestFixtures(): void // The BatchWrite tests are skipped for the GSQL dialect when running // against the emulator. self::skipEmulatorTests(); - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$database->updateDdlBatch([ 'CREATE TABLE Singers ( @@ -57,16 +57,16 @@ public function testBatchWrite() $mutationGroups = []; $mutationGroups[] = self::$database->mutationGroup() ->insertOrUpdate( - "Singers", + 'Singers', ['SingerId' => 16, 'FirstName' => 'Scarlet', 'LastName' => 'Terry'] ); $mutationGroups[] = self::$database->mutationGroup() ->insertOrUpdate( - "Singers", + 'Singers', ['SingerId' => 17, 'FirstName' => 'Marc', 'LastName' => 'Kristen'] )->insertOrUpdate( - "Albums", + 'Albums', ['AlbumId' => 1, 'SingerId' => 17, 'AlbumTitle' => 'Total Junk'] ); diff --git a/Spanner/tests/System/PgOperationsTest.php b/Spanner/tests/System/PgOperationsTest.php index 53238803c875..016a1407eb97 100644 --- a/Spanner/tests/System/PgOperationsTest.php +++ b/Spanner/tests/System/PgOperationsTest.php @@ -42,7 +42,7 @@ public static function setUpTestFixtures(): void if (self::$isSetup) { return; } - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$id = rand(1000, 9999); self::$row = [ diff --git a/Spanner/tests/System/PgPartitionedDmlTest.php b/Spanner/tests/System/PgPartitionedDmlTest.php index 847a7384975a..840cbcdad7e6 100644 --- a/Spanner/tests/System/PgPartitionedDmlTest.php +++ b/Spanner/tests/System/PgPartitionedDmlTest.php @@ -17,8 +17,6 @@ namespace Google\Cloud\Spanner\Tests\System; -use Google\Cloud\Spanner\Tests\System\SpannerPgTestCase; - /** * @group spanner * @group spanner-pdml @@ -28,6 +26,14 @@ class PgPartitionedDmlTest extends SpannerPgTestCase { const PDML_TABLE = 'partitionedDml'; + /** + * @beforeClass + */ + public static function setUpTestFixtures(): void + { + self::setUpTestDatabase(); + } + public function testPdml() { // Skipping temporarily while we figure out the issue with diff --git a/Spanner/tests/System/PgQueryTest.php b/Spanner/tests/System/PgQueryTest.php index bf7d218a0873..2bcf5419c9f6 100644 --- a/Spanner/tests/System/PgQueryTest.php +++ b/Spanner/tests/System/PgQueryTest.php @@ -24,8 +24,8 @@ use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\Interval; -use Google\Cloud\Spanner\PgNumeric; use Google\Cloud\Spanner\PgJsonb; +use Google\Cloud\Spanner\PgNumeric; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; use Google\Cloud\Spanner\V1\RequestOptions\Priority; @@ -46,7 +46,7 @@ class PgQueryTest extends SpannerPgTestCase */ public static function setUpTestFixtures(): void { - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$database->updateDdl( 'CREATE TABLE ' . self::TABLE_NAME . ' ( @@ -64,7 +64,7 @@ public static function setUpTestFixtures(): void )' )->pollUntilComplete(); - self::$timestampVal = new Timestamp(new \DateTime); + self::$timestampVal = new Timestamp(new \DateTime()); self::$database->insertOrUpdateBatch(self::TABLE_NAME, [ [ @@ -315,7 +315,7 @@ public function testBindPgNumericParameter() $row = $res->rows()->current(); $this->assertInstanceOf(PgNumeric::class, $row['age']); $this->assertEquals($str, $val->formatAsString()); - $this->assertEquals($str, (string)$val->get()); + $this->assertEquals($str, (string) $val->get()); } public function testBindPgNumericParameterNull() @@ -361,7 +361,7 @@ public function testBindBytesParameter() $row = $res->rows()->current(); $this->assertInstanceOf(Bytes::class, $row['bytes_col']); $this->assertEquals($str, base64_decode($bytes->formatAsString())); - $this->assertEquals($str, (string)$bytes->get()); + $this->assertEquals($str, (string) $bytes->get()); } public function testBindBytesParameterNull() @@ -440,7 +440,7 @@ public function testBindTimestampParameterNull() public function testBindDateParameter() { - $res = self::$database->execute("SELECT * FROM " . self::TABLE_NAME . " WHERE dt BETWEEN $1 AND $2", [ + $res = self::$database->execute('SELECT * FROM ' . self::TABLE_NAME . ' WHERE dt BETWEEN $1 AND $2', [ 'parameters' => [ 'p1' => new Date(new \DateTime('2020-01-01')), 'p2' => new Date(new \DateTime('2021-01-01')) @@ -515,7 +515,7 @@ public function testBindPgJsonbParameter() $row = $res->rows()->current(); $this->assertInstanceOf(PgJsonb::class, $row['data']); $this->assertEquals($str, $val->formatAsString()); - $this->assertEquals($str, (string)$val->get()); + $this->assertEquals($str, (string) $val->get()); } public function testBindJsonbParameterNull() @@ -553,7 +553,7 @@ public function testBindIntervalParameter() $db = self::$database; $interval = Interval::parse('P1Y2M3DT4H5M6.7S'); - $res = $db->execute("SELECT $1 AS foo", [ + $res = $db->execute('SELECT $1 AS foo', [ 'parameters' => [ 'p1' => $interval ], @@ -571,7 +571,7 @@ public function testBindIntervalParameterNull() { $db = self::$database; - $res = $db->execute("SELECT CAST($1 AS INTERVAL) AS foo", [ + $res = $db->execute('SELECT CAST($1 AS INTERVAL) AS foo', [ 'parameters' => [ 'p1' => null ], @@ -622,16 +622,16 @@ public function arrayTypesProvider() { return [ // boolean - [[true,true,false]], + [[true, true, false]], // int64 - [[5,4,3,2,1]], + [[5, 4, 3, 2, 1]], // float64 [[3.14, 4.13, 1.43]], // string - [['hello','world','google','cloud']], + [['hello', 'world', 'google', 'cloud']], // bytes [ @@ -704,7 +704,7 @@ function (array $res) { [ new PgJsonb('{}'), new PgJsonb('{"a": "b"}'), - new PgJsonb(["a" => "b"]) + new PgJsonb(['a' => 'b']) ], ['{}', '{"a": "b"}', '{"a": "b"}'], PgJsonb::class, @@ -717,21 +717,7 @@ function (array $res) { } ], // pg_oid - [[5,4,3,2,1]], - // Interval - [ - [ - Interval::parse('P1Y'), - Interval::parse('PT1H'), - Interval::parse('P1M') - ], - [ - Interval::parse('P1Y'), - Interval::parse('PT1H'), - Interval::parse('P1M') - ], - Interval::class, - ] + [[5, 4, 3, 2, 1]], ]; } diff --git a/Spanner/tests/System/PgReadTest.php b/Spanner/tests/System/PgReadTest.php index f5d1193a9bbb..b4a9b6600ae8 100644 --- a/Spanner/tests/System/PgReadTest.php +++ b/Spanner/tests/System/PgReadTest.php @@ -39,10 +39,10 @@ class PgReadTest extends SpannerPgTestCase */ public static function setUpTestFixtures(): void { - parent::setUpTestFixtures(); + self::setUpTestDatabase(); - self::$readTableName = "read_table"; - self::$rangeTableName = "range_table"; + self::$readTableName = 'read_table'; + self::$rangeTableName = 'range_table'; $create = 'CREATE TABLE %s ( id bigint NOT NULL, @@ -312,7 +312,7 @@ public function testReadWithLimit() }; $limitCount = count(iterator_to_array($res(10))); - $unlimitCount = count(iterator_to_array($res(null))); + $unlimitCount = count(iterator_to_array($res(0))); $this->assertEquals(10, $limitCount); $this->assertNotEquals($limitCount, $unlimitCount); @@ -331,7 +331,7 @@ public function testReadOverIndexWithLimit() }; $limitCount = count(iterator_to_array($res(10))); - $unlimitCount = count(iterator_to_array($res(null))); + $unlimitCount = count(iterator_to_array($res(0))); $this->assertEquals(10, $limitCount); $this->assertNotEquals($limitCount, $unlimitCount); diff --git a/Spanner/tests/System/PgTransactionTest.php b/Spanner/tests/System/PgTransactionTest.php index f6f807c33173..187211b69e80 100644 --- a/Spanner/tests/System/PgTransactionTest.php +++ b/Spanner/tests/System/PgTransactionTest.php @@ -46,9 +46,9 @@ public static function setUpTestFixtures(): void if (self::$isSetup) { return; } - parent::setUpTestFixtures(); + self::setUpTestDatabase(); - self::$tableName = "transactions_test"; + self::$tableName = 'transactions_test'; self::$database->updateDdlBatch([ 'CREATE TABLE ' . self::$tableName . ' ( diff --git a/Spanner/tests/System/PgWriteTest.php b/Spanner/tests/System/PgWriteTest.php index 3eee888fe28e..1217716956e7 100644 --- a/Spanner/tests/System/PgWriteTest.php +++ b/Spanner/tests/System/PgWriteTest.php @@ -25,9 +25,9 @@ use Google\Cloud\Spanner\CommitTimestamp; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Spanner\Timestamp; -use Google\Cloud\Spanner\PgNumeric; use Google\Cloud\Spanner\PgJsonb; +use Google\Cloud\Spanner\PgNumeric; +use Google\Cloud\Spanner\Timestamp; use Google\Rpc\Code; /** @@ -49,7 +49,7 @@ public static function setUpTestFixtures(): void { // The equiavalent tests for the GSQL dialect are also skipped. self::skipEmulatorTests(); - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$database->updateDdlBatch([ 'CREATE TABLE ' . self::TABLE_NAME . ' ( @@ -98,7 +98,7 @@ public function fieldValueProvider() [$this->randId(), 'datefield', new Date(new \DateTime('1981-01-20'))], [$this->randId(), 'intfield', 787878787], [$this->randId(), 'stringfield', 'foo bar'], - [$this->randId(), 'timestampfield', new Timestamp(new \DateTime)], + [$this->randId(), 'timestampfield', new Timestamp(new \DateTime())], [$this->randId(), 'pgnumericfield', new PgNumeric('0.123456789')], [$this->randId(), 'pgjsonbfield', new PgJsonb('{}')], [$this->randId(), 'pgjsonbfield', new PgJsonb('{"a": 1.1, "b": "def"}')], @@ -242,9 +242,9 @@ public function arrayFieldValueProvider() { return [ [$this->randId(), 'arrayfield', []], - [$this->randId(), 'arrayfield', [1,2,null,4,5]], + [$this->randId(), 'arrayfield', [1, 2, null, 4, 5]], [$this->randId(), 'arrayfield', null], - [$this->randId(), 'arrayboolfield', [true,false]], + [$this->randId(), 'arrayboolfield', [true, false]], [$this->randId(), 'arrayboolfield', []], [$this->randId(), 'arrayboolfield', [true, false, null, false]], [$this->randId(), 'arrayboolfield', null], @@ -256,9 +256,9 @@ public function arrayFieldValueProvider() [$this->randId(), 'arrayfloat4field', []], [$this->randId(), 'arrayfloat4field', [1.1, null, 1.3]], [$this->randId(), 'arrayfloat4field', null], - [$this->randId(), 'arraystringfield', ['foo','bar','baz']], + [$this->randId(), 'arraystringfield', ['foo', 'bar', 'baz']], [$this->randId(), 'arraystringfield', []], - [$this->randId(), 'arraystringfield', ['foo',null,'baz']], + [$this->randId(), 'arraystringfield', ['foo', null, 'baz']], [$this->randId(), 'arraystringfield', null], [$this->randId(), 'arraybytesfield', []], [$this->randId(), 'arraybytesfield', null], @@ -310,13 +310,14 @@ public function testWriteAndReadBackArrayValue($id, $field, $value) public function arrayFieldComplexValueProvider() { + $timestamp = new Timestamp(new \DateTime()); 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)]], - [$this->randId(), 'arraypgnumericfield', [new PgNumeric("0.12345"),null,new PgNumeric("12345")]], - [$this->randId(), 'arraypgjsonbfield', [new PgJsonb('{"a":1.1,"b":"hello"}'),null, - new PgJsonb(["a" => 1, "b" => null]),new PgJsonb('{}'),new PgJsonb([])]], + [$this->randId(), 'arraybytesfield', [new Bytes('foo'), null, new Bytes('baz')]], + [$this->randId(), 'arraytimestampfield', [$timestamp, null, $timestamp]], + [$this->randId(), 'arraydatefield', [new Date(new \DateTime()), null, new Date(new \DateTime())]], + [$this->randId(), 'arraypgnumericfield', [new PgNumeric('0.12345'), null, new PgNumeric('12345')]], + [$this->randId(), 'arraypgjsonbfield', [new PgJsonb('{"a":1.1,"b":"hello"}'), null, + new PgJsonb(['a' => 1, 'b' => null]), new PgJsonb('{}'), new PgJsonb([])]], ]; } @@ -442,11 +443,11 @@ public function testWriteAndReadBackRandomNumeric($id, $numeric) public function randomNumericProvider() { return [ - [$this->randId(), new PgNumeric((string)rand(100, 9999))], - [$this->randId(), new PgNumeric((string)rand(100, 9999))], - [$this->randId(), new PgNumeric((string)rand(100, 9999))], - [$this->randId(), new PgNumeric((string)rand(100, 9999))], - [$this->randId(), new PgNumeric((string)rand(100, 9999))], + [$this->randId(), new PgNumeric((string) rand(100, 9999))], + [$this->randId(), new PgNumeric((string) rand(100, 9999))], + [$this->randId(), new PgNumeric((string) rand(100, 9999))], + [$this->randId(), new PgNumeric((string) rand(100, 9999))], + [$this->randId(), new PgNumeric((string) rand(100, 9999))], ]; } @@ -458,7 +459,7 @@ public function testCommitTimestamp() $id = $this->randId(); $ts = self::$database->insert(self::COMMIT_TIMESTAMP_TABLE_NAME, [ 'id' => $id, - 'committimestamp' => new CommitTimestamp + 'committimestamp' => new CommitTimestamp() ]); $res = self::$database->execute('SELECT * FROM ' . self::COMMIT_TIMESTAMP_TABLE_NAME . ' WHERE id = $1', [ @@ -583,7 +584,7 @@ public function testTimestampPrecisionLocale($timestamp) public function timestamps() { - $today = new \DateTime; + $today = new \DateTime(); $str = $today->format('Y-m-d\TH:i:s'); $todayLowMs = \DateTime::createFromFormat('U.u', time() . '.012345'); @@ -611,17 +612,17 @@ public function testExecuteUpdate() $db = self::$database; $db->runTransaction(function ($t) use ($id, $randStr) { - $count = $t->executeUpdate( - 'INSERT INTO ' . self::TABLE_NAME . ' (id, stringfield) VALUES ($1, $2)', - [ - 'parameters' => [ - 'p1' => $id, - 'p2' => $randStr - ] + $count = $t->executeUpdate( + 'INSERT INTO ' . self::TABLE_NAME . ' (id, stringfield) VALUES ($1, $2)', + [ + 'parameters' => [ + 'p1' => $id, + 'p2' => $randStr ] - ); + ] + ); - $this->assertEquals(1, $count); + $this->assertEquals(1, $count); $row = $t->execute('SELECT * FROM ' . self::TABLE_NAME . ' WHERE id = $1', [ 'parameters' => [ diff --git a/Spanner/tests/System/QueryTest.php b/Spanner/tests/System/QueryTest.php index 7106fd685f3d..8016b1138a60 100644 --- a/Spanner/tests/System/QueryTest.php +++ b/Spanner/tests/System/QueryTest.php @@ -37,6 +37,14 @@ */ class QueryTest extends SpannerTestCase { + /** + * @beforeClass + */ + public static function setUpTestFixtures(): void + { + self::setUpTestDatabase(); + } + /** * covers 19 */ @@ -84,7 +92,7 @@ public function testQueryReturnsArrayStruct() $res = $db->execute('SELECT ARRAY(SELECT STRUCT(1, 2))'); $row = $res->rows()->current(); - $this->assertEquals($row[0][0], [1,2]); + $this->assertEquals($row[0][0], [1, 2]); } /** @@ -251,7 +259,7 @@ public function testBindNumericParameter() $row = $res->rows()->current(); $this->assertInstanceOf(Numeric::class, $row['foo']); $this->assertEquals($str, $numeric->formatAsString()); - $this->assertEquals($str, (string)$numeric->get()); + $this->assertEquals($str, (string) $numeric->get()); } public function testBindNumericParameterNull() @@ -290,7 +298,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()); + $this->assertEquals($str, (string) $bytes->get()); } /** @@ -320,7 +328,7 @@ public function testBindDateParameter() { $db = self::$database; - $ts = new Date(new \DateTimeImmutable); + $ts = new Date(new \DateTimeImmutable()); $res = $db->execute('SELECT @param as foo', [ 'parameters' => [ @@ -376,7 +384,7 @@ public function testBindIntervalParameter() $db = self::$database; $interval = Interval::parse('P1Y2M3DT4H5M6.7S'); - $res = $db->execute("SELECT @param as foo", [ + $res = $db->execute('SELECT @param as foo', [ 'parameters' => [ 'param' => $interval ], @@ -648,16 +656,16 @@ public function arrayTypes() { return [ // boolean (covers 37) - [[true,true,false]], + [[true, true, false]], // int64 (covers 40) - [[5,4,3,2,1]], + [[5, 4, 3, 2, 1]], // float64 (covers 43) [[3.14, 4.13, 1.43]], // string (covers 46) - [['hello','world','google','cloud']], + [['hello', 'world', 'google', 'cloud']], // bytes (covers 49) [ @@ -807,7 +815,7 @@ public function testBindStructParameter() 'p4' => 10 ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('userf', Database::TYPE_STRING) ->add('threadf', Database::TYPE_INT64) ] @@ -831,7 +839,7 @@ public function testBindNullStructParameter() 'structParam' => null ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('userf', Database::TYPE_STRING) ->add('threadf', Database::TYPE_INT64) ] @@ -857,10 +865,10 @@ public function testBindNestedStructParameter() ] ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add( 'structf', - (new StructType)->add('nestedf', Database::TYPE_STRING) + (new StructType())->add('nestedf', Database::TYPE_STRING) ) ] ]); @@ -881,10 +889,10 @@ public function testBindNullNestedStructParameter() 'structParam' => null ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add( 'structf', - (new StructType)->add('nestedf', Database::TYPE_STRING) + (new StructType())->add('nestedf', Database::TYPE_STRING) ) ] ]); @@ -904,7 +912,7 @@ public function testBindEmptyStructParameter() 'structParam' => [] ], 'types' => [ - 'structParam' => new StructType + 'structParam' => new StructType() ] ]); @@ -924,7 +932,7 @@ public function testBindStructNoFieldsParameter() 'structParam' => null ], 'types' => [ - 'structParam' => new StructType + 'structParam' => new StructType() ] ]); @@ -946,7 +954,7 @@ public function testBindStructParameterNullFields() ] ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('f1', Database::TYPE_INT64) ] ]); @@ -969,7 +977,7 @@ public function testBindStructParameterEqualityCheck() ] ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('threadf', Database::TYPE_INT64) ->add('userf', Database::TYPE_STRING) ] @@ -993,7 +1001,7 @@ public function testBindStructParameterNullCheck() ] ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('userf', Database::TYPE_STRING) ->add('threadf', Database::TYPE_INT64) ] @@ -1021,9 +1029,9 @@ public function testBindArrayOfStructsParameter() ], ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('arraysf', new ArrayType( - (new StructType)->add('threadid', Database::TYPE_INT64) + (new StructType())->add('threadid', Database::TYPE_INT64) )) ] ]); @@ -1046,9 +1054,9 @@ public function testBindArrayOfStructsNullParameter() ], ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('arraysf', new ArrayType( - (new StructType)->add('threadid', Database::TYPE_INT64) + (new StructType())->add('threadid', Database::TYPE_INT64) )) ] ]); @@ -1068,9 +1076,9 @@ public function testBindNullArrayOfStructsParameter() 'structParam' => null ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('arraysf', new ArrayType( - (new StructType)->add('threadid', Database::TYPE_INT64) + (new StructType())->add('threadid', Database::TYPE_INT64) )) ] ]); @@ -1087,13 +1095,13 @@ public function testBindArrayOfStructsDuplicateFieldName() $db = self::$database; $res = $db->execute('SELECT * FROM UNNEST(ARRAY(SELECT @structParam))', [ 'parameters' => [ - 'structParam' => (new StructValue) + 'structParam' => (new StructValue()) ->add('hello', 'world') ->add('foo', 'bar') ->add('foo', 2) ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('hello', Database::TYPE_STRING) ->add('foo', Database::TYPE_STRING) ->add('foo', Database::TYPE_INT64) @@ -1124,15 +1132,15 @@ public function testBindStructWithMixedUnnamedParameters() $db = self::$database; $res = $db->execute('SELECT * FROM UNNEST(ARRAY(SELECT @structParam))', [ 'parameters' => [ - 'structParam' => (new StructValue) + 'structParam' => (new StructValue()) ->addUnnamed(1) ->add('f1', 2) ->addUnnamed([ - 'a','b','c' + 'a', 'b', 'c' ]) ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->addUnnamed(Database::TYPE_INT64) ->add('f1', Database::TYPE_INT64) ->addUnnamed(new ArrayType(Database::TYPE_STRING)) @@ -1142,7 +1150,7 @@ public function testBindStructWithMixedUnnamedParameters() $this->assertEquals(1, $res[0]); $this->assertEquals(2, $res['f1']); $this->assertEquals([ - 'a','b','c' + 'a', 'b', 'c' ], $res[2]); } @@ -1154,16 +1162,16 @@ public function testBindStructWithAllUnnamedParameters() $db = self::$database; $res = $db->execute('SELECT * FROM UNNEST(ARRAY(SELECT @structParam))', [ 'parameters' => [ - 'structParam' => (new StructValue) + 'structParam' => (new StructValue()) ->addUnnamed(1) ->addUnnamed('field') ->addUnnamed([ - 'a','b','c' + 'a', 'b', 'c' ]) ->addUnnamed(false) ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->addUnnamed(Database::TYPE_INT64) ->addUnnamed(Database::TYPE_STRING) ->addUnnamed(new ArrayType(Database::TYPE_STRING)) @@ -1174,7 +1182,7 @@ public function testBindStructWithAllUnnamedParameters() $this->assertEquals(1, $res[0]); $this->assertEquals('field', $res[1]); $this->assertEquals([ - 'a','b','c' + 'a', 'b', 'c' ], $res[2]); $this->assertFalse($res[3]); } @@ -1193,7 +1201,7 @@ public function testBindStructInferredParameterTypes() 'structParam' => $values ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('str', Database::TYPE_STRING) ] ])->rows()->current(); @@ -1212,14 +1220,14 @@ public function testBindStructInferredParameterTypesWithUnnamed() $db = self::$database; $res = $db->execute('SELECT * FROM UNNEST(ARRAY(SELECT @structParam))', [ 'parameters' => [ - 'structParam' => (new StructValue) + 'structParam' => (new StructValue()) ->add('arr', ['a', 'b']) ->addUnnamed('hello') ->addUnnamed(10) ->add('str', 'world') ], 'types' => [ - 'structParam' => (new StructType) + 'structParam' => (new StructType()) ->add('str', Database::TYPE_STRING) ] ])->rows(Result::RETURN_NAME_VALUE_PAIR)->current(); diff --git a/Spanner/tests/System/README.md b/Spanner/tests/System/README.md new file mode 100644 index 000000000000..2574d1df4147 --- /dev/null +++ b/Spanner/tests/System/README.md @@ -0,0 +1,27 @@ +# Google Cloud Spanner System Tests + +## Run the system tests + +### Set the environment variables + +```bash +# These environment variables are required +GOOGLE_CLOUD_PHP_TESTS_KEY_PATH="/path/to/service-account.json" +GOOGLE_CLOUD_PHP_WHITELIST_TESTS_KEY_PATH="<SAME AS ABOVE>" +GOOGLE_CLOUD_PROJECT="<YOUR_PROJECT_ID>" + +# These environment variables are optional, and will speed up running the tests locally +GOOGLE_CLOUD_SPANNER_TEST_DATABASE=test-database +GOOGLE_CLOUD_SPANNER_TEST_BACKUP_DATABASE_1=test-backup-database1 +GOOGLE_CLOUD_SPANNER_TEST_BACKUP_DATABASE_2=test-backup-database2 +``` + +### Run PHPUnit + +``` +vendor/bin/phpunit -c phpunit-system.xml.dist --stop-on-failure tests/System/BatchTest.php +``` + +## Run the emulator + +Some tests ONLY run against the emulator. To run those, you'll need to run the emulator locally. \ No newline at end of file diff --git a/Spanner/tests/System/ReadTest.php b/Spanner/tests/System/ReadTest.php index 47cca1e79f96..3335f0dc5c2d 100644 --- a/Spanner/tests/System/ReadTest.php +++ b/Spanner/tests/System/ReadTest.php @@ -43,7 +43,7 @@ class ReadTest extends SpannerTestCase */ public static function setUpTestFixtures(): void { - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$readTableName = uniqid(self::TESTING_PREFIX); self::$rangeTableName = uniqid(self::TESTING_PREFIX); @@ -405,7 +405,7 @@ public function testReadWithLimit() }; $limitCount = count(iterator_to_array($res(10))); - $unlimitCount = count(iterator_to_array($res(null))); + $unlimitCount = count(iterator_to_array($res(0))); $this->assertEquals(10, $limitCount); $this->assertNotEquals($limitCount, $unlimitCount); @@ -427,7 +427,7 @@ public function testReadOverIndexWithLimit() }; $limitCount = count(iterator_to_array($res(10))); - $unlimitCount = count(iterator_to_array($res(null))); + $unlimitCount = count(iterator_to_array($res(0))); $this->assertEquals(10, $limitCount); $this->assertNotEquals($limitCount, $unlimitCount); diff --git a/Spanner/tests/System/SessionTest.php b/Spanner/tests/System/SessionTest.php index 6fe694880053..863e3aa5bfd5 100644 --- a/Spanner/tests/System/SessionTest.php +++ b/Spanner/tests/System/SessionTest.php @@ -28,6 +28,14 @@ */ class SessionTest extends SpannerTestCase { + /** + * @beforeClass + */ + public static function setUpTestFixtures(): void + { + self::setUpTestDatabase(); + } + public function testCacheSessionPool() { $identity = self::$database->identity(); @@ -38,7 +46,7 @@ public function testCacheSessionPool() $identity['database'] ); - $cache = new MemoryCacheItemPool; + $cache = new MemoryCacheItemPool(); $pool = new CacheSessionPool($cache, [ 'maxSessions' => 10, 'minSessions' => 5, @@ -105,7 +113,7 @@ public function testSessionPoolShouldFailWhenIncorrectDatabase() ['maxCyclesToWaitForSession' => 1] ); $db->runTransaction(function ($t) { - $t->select("SELECT 1"); + $t->select('SELECT 1'); $t->commit(); }); } diff --git a/Spanner/tests/System/SnapshotTest.php b/Spanner/tests/System/SnapshotTest.php index 40509ec6b0ea..8ed0063d91ef 100644 --- a/Spanner/tests/System/SnapshotTest.php +++ b/Spanner/tests/System/SnapshotTest.php @@ -18,12 +18,11 @@ namespace Google\Cloud\Spanner\Tests\System; use Google\Cloud\Core\Exception\BadRequestException; -use Google\Cloud\Spanner\Date; -use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\V1\ReadRequest\LockHint; use Google\Cloud\Spanner\V1\ReadRequest\OrderBy; +use Google\Protobuf\Duration; /** * @group spanner @@ -40,7 +39,7 @@ class SnapshotTest extends SpannerTestCase */ public static function setUpTestFixtures(): void { - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$tableName = uniqid(self::TABLE_NAME); @@ -95,7 +94,7 @@ public function testSnapshotExactTimestampRead() $db->insert(self::$tableName, $row); sleep(1); - $ts = new Timestamp(new \DateTimeImmutable); + $ts = new Timestamp(new \DateTimeImmutable()); sleep(1); $newRow = $row; @@ -160,14 +159,14 @@ public function testSnapshotExactStaleness() $db->insert(self::$tableName, $row); sleep(1); - $ts = new Timestamp(new \DateTimeImmutable); + $ts = new Timestamp(new \DateTimeImmutable()); sleep(1); $newRow = $row; $newRow['number'] = 2; $db->replace(self::$tableName, $newRow); - $duration = new Duration(1); + $duration = new Duration(['seconds' => 1, 'nanos' => 0]); $snapshot = $db->snapshot([ 'exactStaleness' => $duration, @@ -195,14 +194,14 @@ public function testSnapshotMaxStaleness() $db->insert(self::$tableName, $row); sleep(1); - $ts = new Timestamp(new \DateTimeImmutable); + $ts = new Timestamp(new \DateTimeImmutable()); sleep(1); $newRow = $row; $newRow['number'] = 2; $db->replace(self::$tableName, $newRow); - $duration = new Duration(1); + $duration = new Duration(['seconds' => 1, 'nanos' => 0]); $snapshot = $db->snapshot([ 'maxStaleness' => $duration, @@ -224,7 +223,7 @@ public function testSnapshotMinReadTimestampFails() $db = self::$database; $db->snapshot([ - 'minReadTimestamp' => new Timestamp(new \DateTimeImmutable) + 'minReadTimestamp' => new Timestamp(new \DateTimeImmutable()) ]); } @@ -238,7 +237,7 @@ public function testSnapshotMaxStalenessFails() $db = self::$database; $db->snapshot([ - 'maxStaleness' => new Duration(1) + 'maxStaleness' => new Duration(['seconds' => 1, 'nanos' => 0]) ]); } diff --git a/Spanner/tests/System/SpannerPgTestCase.php b/Spanner/tests/System/SpannerPgTestCase.php index d80cf4786a9c..95d270d8d1d4 100644 --- a/Spanner/tests/System/SpannerPgTestCase.php +++ b/Spanner/tests/System/SpannerPgTestCase.php @@ -17,12 +17,12 @@ namespace Google\Cloud\Spanner\Tests\System; +use Google\Auth\Cache\MemoryCacheItemPool; use Google\Cloud\Core\Testing\System\SystemTestCase; use Google\Cloud\Spanner; -use Google\Cloud\Spanner\SpannerClient; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; use Google\Cloud\Spanner\Session\CacheSessionPool; -use Google\Auth\Cache\MemoryCacheItemPool; +use Google\Cloud\Spanner\SpannerClient; /** * @group spanner @@ -47,10 +47,7 @@ abstract class SpannerPgTestCase extends SystemTestCase private static $hasSetUp = false; - /** - * @beforeClass - */ - public static function setUpTestFixtures(): void + protected static function setUpTestDatabase(): void { if (self::$hasSetUp) { return; @@ -90,7 +87,7 @@ public static function setUpTestFixtures(): void // Currently, the emulator doesn't support setting roles for the PG // dialect. - if (!getenv("SPANNER_EMULATOR_HOST")) { + if (!getenv('SPANNER_EMULATOR_HOST')) { $db->updateDdlBatch( [ 'CREATE ROLE ' . self::DATABASE_ROLE, @@ -114,7 +111,7 @@ public static function getDatabaseFromInstance($instance, $dbName, $options = [] public static function getDatabaseWithSessionPool($dbName, $options = []) { - $sessionCache = new MemoryCacheItemPool; + $sessionCache = new MemoryCacheItemPool(); $sessionPool = new CacheSessionPool( $sessionCache, $options @@ -136,7 +133,7 @@ public static function getDatabaseInstance($dbName) public static function skipEmulatorTests() { - if ((bool) getenv("SPANNER_EMULATOR_HOST")) { + if ((bool) getenv('SPANNER_EMULATOR_HOST')) { self::markTestSkipped('This test is not supported by the emulator.'); } } @@ -191,7 +188,7 @@ private static function getClient() $clientConfig['gapicSpannerInstanceAdminClient'] = new Spanner\Admin\Instance\V1\InstanceAdminClient($gapicConfig); - echo "Using Service Address: ". $serviceAddress . PHP_EOL; + echo 'Using Service Address: ' . $serviceAddress . PHP_EOL; } self::$client = new SpannerClient($clientConfig); diff --git a/Spanner/tests/System/SpannerTestCase.php b/Spanner/tests/System/SpannerTestCase.php index d74467a5237f..0317707f2e7a 100644 --- a/Spanner/tests/System/SpannerTestCase.php +++ b/Spanner/tests/System/SpannerTestCase.php @@ -17,12 +17,12 @@ namespace Google\Cloud\Spanner\Tests\System; +use Google\Auth\Cache\MemoryCacheItemPool; use Google\Cloud\Core\Testing\System\SystemTestCase; use Google\Cloud\Spanner; -use Google\Cloud\Spanner\SpannerClient; -use Google\Cloud\Spanner\Session\CacheSessionPool; -use Google\Auth\Cache\MemoryCacheItemPool; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; +use Google\Cloud\Spanner\Session\CacheSessionPool; +use Google\Cloud\Spanner\SpannerClient; /** * @group spanner @@ -46,10 +46,7 @@ abstract class SpannerTestCase extends SystemTestCase private static $hasSetUp = false; - /** - * @beforeClass - */ - public static function setUpTestFixtures(): void + protected static function setUpTestDatabase(): void { if (self::$hasSetUp) { return; @@ -59,45 +56,45 @@ public static function setUpTestFixtures(): void self::$instance = self::$client->instance(self::INSTANCE_NAME); - self::$dbName = uniqid(self::TESTING_PREFIX); - $op = self::$instance->createDatabase(self::$dbName); - $op->pollUntilComplete(); - - $db = self::getDatabaseInstance(self::$dbName); - - self::$deletionQueue->add(function () use ($db) { - $db->drop(); - }); - - $op = $db->updateDdlBatch( - [ - 'CREATE TABLE ' . self::TEST_TABLE_NAME . ' ( - id INT64 NOT NULL, - name STRING(MAX) NOT NULL, - birthday DATE - ) PRIMARY KEY (id)', - 'CREATE UNIQUE INDEX ' . self::TEST_INDEX_NAME . ' - ON ' . self::TEST_TABLE_NAME . ' (name)', - ] - ); - $op->pollUntilComplete(); - - self::$database = $db; - self::$database2 = self::getDatabaseInstance(self::$dbName); + if (!self::$dbName = getenv('GOOGLE_CLOUD_SPANNER_TEST_DATABASE')) { + self::$dbName = uniqid(self::TESTING_PREFIX); + self::$deletionQueue->add(function () { + self::getDatabaseInstance(self::$dbName)->drop(); + }); + } + self::$database = self::getDatabaseInstance(self::$dbName); - if ($db->info()['databaseDialect'] == DatabaseDialect::GOOGLE_STANDARD_SQL) { - $db->updateDdlBatch( + if (!self::$database->exists()) { + $op = self::$instance->createDatabase(self::$dbName); + $op->pollUntilComplete(); + $op = self::$database->updateDdlBatch( [ - 'CREATE ROLE ' . self::DATABASE_ROLE, - 'CREATE ROLE ' . self::RESTRICTIVE_DATABASE_ROLE, - 'GRANT SELECT ON TABLE ' . self::TEST_TABLE_NAME . - ' TO ROLE ' . self::DATABASE_ROLE, - 'GRANT SELECT(id, name), INSERT(id, name), UPDATE(id, name) ON TABLE ' - . self::TEST_TABLE_NAME . ' TO ROLE ' . self::RESTRICTIVE_DATABASE_ROLE, + 'CREATE TABLE ' . self::TEST_TABLE_NAME . ' ( + id INT64 NOT NULL, + name STRING(MAX) NOT NULL, + birthday DATE + ) PRIMARY KEY (id)', + 'CREATE UNIQUE INDEX ' . self::TEST_INDEX_NAME . ' + ON ' . self::TEST_TABLE_NAME . ' (name)', ] - )->pollUntilComplete(); + ); + $op->pollUntilComplete(); + + if (self::$database->info()['databaseDialect'] == DatabaseDialect::GOOGLE_STANDARD_SQL) { + self::$database->updateDdlBatch( + [ + 'CREATE ROLE ' . self::DATABASE_ROLE, + 'CREATE ROLE ' . self::RESTRICTIVE_DATABASE_ROLE, + 'GRANT SELECT ON TABLE ' . self::TEST_TABLE_NAME . + ' TO ROLE ' . self::DATABASE_ROLE, + 'GRANT SELECT(id, name), INSERT(id, name), UPDATE(id, name) ON TABLE ' + . self::TEST_TABLE_NAME . ' TO ROLE ' . self::RESTRICTIVE_DATABASE_ROLE, + ] + )->pollUntilComplete(); + } } + self::$database2 = self::getDatabaseInstance(self::$dbName); self::$hasSetUp = true; } @@ -119,13 +116,13 @@ private static function getClient() 'serviceAddress' => $serviceAddress ]; - $clientConfig['gapicSpannerClient'] = new Spanner\V1\SpannerClient($gapicConfig); + $clientConfig['gapicSpannerClient'] = new Spanner\V1\Client\SpannerClient($gapicConfig); $clientConfig['gapicSpannerDatabaseAdminClient'] = - new Spanner\Admin\Database\V1\DatabaseAdminClient($gapicConfig); + new Spanner\Admin\Database\V1\Client\DatabaseAdminClient($gapicConfig); $clientConfig['gapicSpannerInstanceAdminClient'] = - new Spanner\Admin\Instance\V1\InstanceAdminClient($gapicConfig); + new Spanner\Admin\Instance\V1\Client\InstanceAdminClient($gapicConfig); - echo "Using Service Address: ". $serviceAddress . PHP_EOL; + echo 'Using Service Address: ' . $serviceAddress . PHP_EOL; } self::$client = new SpannerClient($clientConfig); @@ -146,7 +143,7 @@ public static function getDatabaseFromInstance($instance, $dbName, $options = [] public static function getDatabaseWithSessionPool($dbName, $options = []) { - $sessionCache = new MemoryCacheItemPool; + $sessionCache = new MemoryCacheItemPool(); $sessionPool = new CacheSessionPool( $sessionCache, $options @@ -163,7 +160,7 @@ public static function getDatabaseWithSessionPool($dbName, $options = []) public static function skipEmulatorTests() { - if ((bool) getenv("SPANNER_EMULATOR_HOST")) { + if ((bool) getenv('SPANNER_EMULATOR_HOST')) { self::markTestSkipped('This test is not supported by the emulator.'); } } diff --git a/Spanner/tests/System/TransactionTest.php b/Spanner/tests/System/TransactionTest.php index 0f5bdc968d2d..3011d11690ee 100644 --- a/Spanner/tests/System/TransactionTest.php +++ b/Spanner/tests/System/TransactionTest.php @@ -17,14 +17,14 @@ namespace Google\Cloud\Spanner\Tests\System; +use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type as ReplicaType; -use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; use Google\Cloud\Spanner\V1\ReadRequest\LockHint; use Google\Cloud\Spanner\V1\ReadRequest\OrderBy; +use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; /** * @group spanner @@ -50,7 +50,7 @@ public static function setUpTestFixtures(): void if (self::$isSetup) { return; } - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$tableName = uniqid(self::TABLE_NAME); self::$id1 = rand(1000, 9999); @@ -82,7 +82,7 @@ public function testRunTransaction() $row = [ 'id' => $id, 'name' => uniqid(self::TESTING_PREFIX), - 'birthday' => new Date(new \DateTime) + 'birthday' => new Date(new \DateTime()) ]; $cols = array_keys($row); @@ -117,7 +117,6 @@ public function testConcurrentTransactionsIncrementValueWithRead() 'number' => 0 ]); - $iterations = shell_exec(implode(' ', [ 'php', __DIR__ . '/pcntl/ConcurrentTransactionsIncrementValueWithRead.php', @@ -398,7 +397,7 @@ public function testRunTransactionILBWithMultipleOperations() $row = [ 'id' => $id, 'name' => uniqid(self::TESTING_PREFIX), - 'birthday' => new Date(new \DateTime) + 'birthday' => new Date(new \DateTime()) ]; // Representative of all mutations $t->insert(self::TEST_TABLE_NAME, $row); @@ -411,7 +410,7 @@ public function testRunTransactionILBWithMultipleOperations() 'parameters' => [ 'id' => $id, 'name' => uniqid(self::TESTING_PREFIX), - 'birthday' => new Date(new \DateTime) + 'birthday' => new Date(new \DateTime()) ], 'transaction' => [ 'begin' => [ diff --git a/Spanner/tests/System/UniverseDomainTest.php b/Spanner/tests/System/UniverseDomainTest.php index 0e7a093412aa..c010f4cef4fb 100644 --- a/Spanner/tests/System/UniverseDomainTest.php +++ b/Spanner/tests/System/UniverseDomainTest.php @@ -17,10 +17,10 @@ namespace Google\Cloud\Spanner\Tests\System; -use Google\Cloud\Core\Testing\System\SystemTestCase; use Google\Cloud\Core\LongRunning\LongRunningOperation; -use Google\Cloud\Spanner\SpannerClient; +use Google\Cloud\Core\Testing\System\SystemTestCase; use Google\Cloud\Spanner\KeySet; +use Google\Cloud\Spanner\SpannerClient; class UniverseDomainTest extends SystemTestCase { @@ -46,7 +46,7 @@ public static function setUpTestFixtures(): void } self::$client = new SpannerClient([ - 'keyFilePath' => $keyFilePath, + 'credentials' => $keyFilePath, 'projectId' => $credentials['project_id'] ?? null, 'universeDomain' => $credentials['universe_domain'] ?? null ]); @@ -74,7 +74,7 @@ public function testCreateInstanceWithUniverseDomain() ]); $op->pollUntilComplete(); - $this->assertEquals(LongRunningOperation::STATE_SUCCESS, $op->state()); + $this->assertEquals(LongRunningOperation::STATE_SUCCESS, $op->state(), json_encode($op->error())); self::$instance = self::$client->instance(self::$instanceId); $info = self::$instance->info(); diff --git a/Spanner/tests/System/WriteTest.php b/Spanner/tests/System/WriteTest.php index 9717c19009d5..f56fa8e73717 100644 --- a/Spanner/tests/System/WriteTest.php +++ b/Spanner/tests/System/WriteTest.php @@ -25,11 +25,10 @@ use Google\Cloud\Spanner\CommitTimestamp; use Google\Cloud\Spanner\Date; use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Numeric; -use Google\Cloud\Spanner\Proto; -use Google\Rpc\Code; +use Google\Cloud\Spanner\Timestamp; use Google\Protobuf\Internal\Message; +use Google\Rpc\Code; use Testing\Data\User; /** @@ -49,7 +48,7 @@ class WriteTest extends SpannerTestCase public static function setUpTestFixtures(): void { self::skipEmulatorTests(); - parent::setUpTestFixtures(); + self::setUpTestDatabase(); self::$database->updateDdlBatch([ 'CREATE PROTO BUNDLE (' . @@ -95,7 +94,7 @@ public function fieldValueProvider() return [ [$this->randId(), 'boolField', false], [$this->randId(), 'boolField', true], - [$this->randId(), 'arrayField', [1,2,3,4,5]], + [$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], @@ -105,9 +104,8 @@ public function fieldValueProvider() [$this->randId(), 'float32Field', -INF], [$this->randId(), 'intField', 787878787], [$this->randId(), 'stringField', 'foo bar'], - [$this->randId(), 'timestampField', new Timestamp(new \DateTime)], - [$this->randId(), 'numericField', new Numeric('0.123456789')], - [$this->randId(), 'protoField', new User(['name' => 'John Doe'])], + [$this->randId(), 'timestampField', new Timestamp(new \DateTime())], + [$this->randId(), 'numericField', new Numeric('0.123456789')] ]; } @@ -272,9 +270,9 @@ public function arrayFieldValueProvider() { return [ [$this->randId(), 'arrayField', []], - [$this->randId(), 'arrayField', [1,2,null,4,5]], + [$this->randId(), 'arrayField', [1, 2, null, 4, 5]], [$this->randId(), 'arrayField', null], - [$this->randId(), 'arrayBoolField', [true,false]], + [$this->randId(), 'arrayBoolField', [true, false]], [$this->randId(), 'arrayBoolField', []], [$this->randId(), 'arrayBoolField', [true, false, null, false]], [$this->randId(), 'arrayBoolField', null], @@ -286,9 +284,9 @@ public function arrayFieldValueProvider() [$this->randId(), 'arrayFloat32Field', []], [$this->randId(), 'arrayFloat32Field', [1.1, null, 1.3]], [$this->randId(), 'arrayFloat32Field', null], - [$this->randId(), 'arrayStringField', ['foo','bar','baz']], + [$this->randId(), 'arrayStringField', ['foo', 'bar', 'baz']], [$this->randId(), 'arrayStringField', []], - [$this->randId(), 'arrayStringField', ['foo',null,'baz']], + [$this->randId(), 'arrayStringField', ['foo', null, 'baz']], [$this->randId(), 'arrayStringField', null], [$this->randId(), 'arrayBytesField', []], [$this->randId(), 'arrayBytesField', null], @@ -359,7 +357,7 @@ public function testWriteAndReadBackFancyArrayValue($id, $field, $value) if ($value instanceof Bytes) { $this->assertEquals($value->formatAsString(), $row[$field]->formatAsString()); } else { - if ($field === 'arrayProtoField') { + if ($field === 'arrayProtoField' && $value !== null) { foreach ($row[$field] as $i => $protoItem) { $row[$field][$i] = $protoItem->get(); } @@ -370,11 +368,12 @@ public function testWriteAndReadBackFancyArrayValue($id, $field, $value) public function arrayFieldComplexValueProvider() { + $timestamp = new Timestamp(new \DateTime()); 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)]], - [$this->randId(), 'arrayNumericField', [new Numeric("0.12345"),null,new NUMERIC("12345")]], + [$this->randId(), 'arrayBytesField', [new Bytes('foo'), null, new Bytes('baz')]], + [$this->randId(), 'arrayTimestampField', [$timestamp, null, $timestamp]], + [$this->randId(), 'arrayDateField', [new Date(new \DateTime()), null, new Date(new \DateTime())]], + [$this->randId(), 'arrayNumericField', [new Numeric('0.12345'), null, new NUMERIC('12345')]], ]; } @@ -508,11 +507,11 @@ public function randomNumericProvider() } return [ - [$this->randId(), new Numeric((string)rand(100, 9999))], - [$this->randId(), new Numeric((string)rand(100, 9999))], - [$this->randId(), new Numeric((string)rand(100, 9999))], - [$this->randId(), new Numeric((string)rand(100, 9999))], - [$this->randId(), new Numeric((string)rand(100, 9999))], + [$this->randId(), new Numeric((string) rand(100, 9999))], + [$this->randId(), new Numeric((string) rand(100, 9999))], + [$this->randId(), new Numeric((string) rand(100, 9999))], + [$this->randId(), new Numeric((string) rand(100, 9999))], + [$this->randId(), new Numeric((string) rand(100, 9999))], ]; } @@ -524,7 +523,7 @@ public function testCommitTimestamp() $id = $this->randId(); $ts = self::$database->insert(self::COMMIT_TIMESTAMP_TABLE_NAME, [ 'id' => $id, - 'commitTimestamp' => new CommitTimestamp + 'commitTimestamp' => new CommitTimestamp() ]); $res = self::$database->execute('SELECT * FROM ' . self::COMMIT_TIMESTAMP_TABLE_NAME . ' WHERE id = @id', [ @@ -649,7 +648,7 @@ public function testTimestampPrecisionLocale($timestamp) public function timestamps() { - $today = new \DateTime; + $today = new \DateTime(); $str = $today->format('Y-m-d\TH:i:s'); $todayLowMs = \DateTime::createFromFormat('U.u', time() . '.012345'); @@ -677,17 +676,17 @@ public function testExecuteUpdate() $db = self::$database; $db->runTransaction(function ($t) use ($id, $randStr) { - $count = $t->executeUpdate( - 'INSERT INTO ' . self::TABLE_NAME . ' (id, stringField) VALUES (@id, @string)', - [ - 'parameters' => [ - 'id' => $id, - 'string' => $randStr - ] + $count = $t->executeUpdate( + 'INSERT INTO ' . self::TABLE_NAME . ' (id, stringField) VALUES (@id, @string)', + [ + 'parameters' => [ + 'id' => $id, + 'string' => $randStr ] - ); + ] + ); - $this->assertEquals(1, $count); + $this->assertEquals(1, $count); $row = $t->execute('SELECT * FROM ' . self::TABLE_NAME . ' WHERE id = @id', [ 'parameters' => [ diff --git a/Spanner/tests/System/pcntl/AbortedErrorCausesRetry.php b/Spanner/tests/System/pcntl/AbortedErrorCausesRetry.php index 590cd55fd0a2..9dfbb3639f86 100644 --- a/Spanner/tests/System/pcntl/AbortedErrorCausesRetry.php +++ b/Spanner/tests/System/pcntl/AbortedErrorCausesRetry.php @@ -6,7 +6,7 @@ use Google\Cloud\Core\Exception\AbortedException; use Google\Cloud\Spanner\Tests\System\SpannerTestCase; -list ($dbName, $tableName, $id) = getInputArgs(); +list($dbName, $tableName, $id) = getInputArgs(); $delay = 5000; if ($childPID1 = pcntl_fork()) { diff --git a/Spanner/tests/System/pcntl/ConcurrentTransactionsIncrementValueWithExecute.php b/Spanner/tests/System/pcntl/ConcurrentTransactionsIncrementValueWithExecute.php index 7f14099acacd..37b8cba7c27f 100644 --- a/Spanner/tests/System/pcntl/ConcurrentTransactionsIncrementValueWithExecute.php +++ b/Spanner/tests/System/pcntl/ConcurrentTransactionsIncrementValueWithExecute.php @@ -5,7 +5,7 @@ use Google\Cloud\Spanner\Tests\System\SpannerTestCase; -list ($dbName, $tableName, $id) = getInputArgs(); +list($dbName, $tableName, $id) = getInputArgs(); $tmpFile = sys_get_temp_dir() . '/ConcurrentTransactionsIncremementValueWithExecute.txt'; setupIterationTracker($tmpFile); @@ -22,7 +22,7 @@ ] ])->rows()->current(); - $row['number'] +=1; + $row['number'] += 1; $transaction->update($tableName, $row); $transaction->commit(); diff --git a/Spanner/tests/System/pcntl/ConcurrentTransactionsIncrementValueWithRead.php b/Spanner/tests/System/pcntl/ConcurrentTransactionsIncrementValueWithRead.php index 7fd596ac369f..5355d50bcc15 100644 --- a/Spanner/tests/System/pcntl/ConcurrentTransactionsIncrementValueWithRead.php +++ b/Spanner/tests/System/pcntl/ConcurrentTransactionsIncrementValueWithRead.php @@ -6,13 +6,13 @@ use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Tests\System\SpannerTestCase; -list ($dbName, $tableName, $id) = getInputArgs(); +list($dbName, $tableName, $id) = getInputArgs(); $tmpFile = sys_get_temp_dir() . '/ConcurrentTransactionsIncremementValueWithRead.txt'; setupIterationTracker($tmpFile); $keyset = new KeySet(['keys' => [$id]]); -$columns = ['id','number']; +$columns = ['id', 'number']; $callable = function ($dbName, KeySet $keyset, array $columns, $tableName) use ($tmpFile) { $iterations = 0; @@ -21,7 +21,7 @@ $iterations++; $row = $transaction->read($tableName, $keyset, $columns)->rows()->current(); - $row['number'] +=1; + $row['number'] += 1; $transaction->update($tableName, $row); $transaction->commit(); diff --git a/Spanner/tests/Unit/Admin/Database/V1/Client/DatabaseAdminClientTest.php b/Spanner/tests/Unit/Admin/Database/V1/Client/DatabaseAdminClientTest.php index 60a75dc0e238..a65c46f36bb6 100644 --- a/Spanner/tests/Unit/Admin/Database/V1/Client/DatabaseAdminClientTest.php +++ b/Spanner/tests/Unit/Admin/Database/V1/Client/DatabaseAdminClientTest.php @@ -1,6 +1,6 @@ <?php /* - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Spanner/tests/Unit/Admin/Database/V1/DatabaseAdminClientTest.php b/Spanner/tests/Unit/Admin/Database/V1/DatabaseAdminClientTest.php deleted file mode 100644 index 9a9c51715033..000000000000 --- a/Spanner/tests/Unit/Admin/Database/V1/DatabaseAdminClientTest.php +++ /dev/null @@ -1,2239 +0,0 @@ -<?php -/* - * Copyright 2018 Google LLC - * - * 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 - * - * https://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. - */ - -/* - * GENERATED CODE WARNING - * This file was automatically generated - do not edit! - */ - -namespace Google\Cloud\Spanner\Admin\Database\Tests\Unit\V1; - -use Google\ApiCore\ApiException; -use Google\ApiCore\CredentialsWrapper; -use Google\ApiCore\LongRunning\OperationsClient; -use Google\ApiCore\Testing\GeneratedTest; -use Google\ApiCore\Testing\MockTransport; -use Google\Cloud\Iam\V1\Policy; -use Google\Cloud\Iam\V1\TestIamPermissionsResponse; -use Google\Cloud\Spanner\Admin\Database\V1\AddSplitPointsResponse; -use Google\Cloud\Spanner\Admin\Database\V1\Backup; -use Google\Cloud\Spanner\Admin\Database\V1\BackupSchedule; -use Google\Cloud\Spanner\Admin\Database\V1\Database; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseRole; -use Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseDdlResponse; -use Google\Cloud\Spanner\Admin\Database\V1\InternalUpdateGraphOperationResponse; -use Google\Cloud\Spanner\Admin\Database\V1\ListBackupOperationsResponse; -use Google\Cloud\Spanner\Admin\Database\V1\ListBackupSchedulesResponse; -use Google\Cloud\Spanner\Admin\Database\V1\ListBackupsResponse; -use Google\Cloud\Spanner\Admin\Database\V1\ListDatabaseOperationsResponse; -use Google\Cloud\Spanner\Admin\Database\V1\ListDatabaseRolesResponse; -use Google\Cloud\Spanner\Admin\Database\V1\ListDatabasesResponse; -use Google\LongRunning\GetOperationRequest; -use Google\LongRunning\Operation; -use Google\Protobuf\Any; -use Google\Protobuf\FieldMask; -use Google\Protobuf\GPBEmpty; -use Google\Protobuf\Timestamp; -use Google\Rpc\Code; -use stdClass; - -/** - * @group database - * - * @group gapic - */ -class DatabaseAdminClientTest extends GeneratedTest -{ - /** @return TransportInterface */ - private function createTransport($deserialize = null) - { - return new MockTransport($deserialize); - } - - /** @return CredentialsWrapper */ - private function createCredentials() - { - return $this->getMockBuilder(CredentialsWrapper::class)->disableOriginalConstructor()->getMock(); - } - - /** @return DatabaseAdminClient */ - private function createClient(array $options = []) - { - $options += [ - 'credentials' => $this->createCredentials(), - ]; - return new DatabaseAdminClient($options); - } - - /** @test */ - public function addSplitPointsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new AddSplitPointsResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $splitPoints = []; - $response = $gapicClient->addSplitPoints($formattedDatabase, $splitPoints); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/AddSplitPoints', $actualFuncCall); - $actualValue = $actualRequestObject->getDatabase(); - $this->assertProtobufEquals($formattedDatabase, $actualValue); - $actualValue = $actualRequestObject->getSplitPoints(); - $this->assertProtobufEquals($splitPoints, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function addSplitPointsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $splitPoints = []; - try { - $gapicClient->addSplitPoints($formattedDatabase, $splitPoints); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function copyBackupTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/copyBackupTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $database = 'database1789464955'; - $name = 'name3373707'; - $sizeBytes = 1796325715; - $freeableSizeBytes = 1302251206; - $exclusiveSizeBytes = 1085921554; - $incrementalBackupChainId = 'incrementalBackupChainId-792099119'; - $expectedResponse = new Backup(); - $expectedResponse->setDatabase($database); - $expectedResponse->setName($name); - $expectedResponse->setSizeBytes($sizeBytes); - $expectedResponse->setFreeableSizeBytes($freeableSizeBytes); - $expectedResponse->setExclusiveSizeBytes($exclusiveSizeBytes); - $expectedResponse->setIncrementalBackupChainId($incrementalBackupChainId); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/copyBackupTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $backupId = 'backupId1355353272'; - $formattedSourceBackup = $gapicClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - $expireTime = new Timestamp(); - $response = $gapicClient->copyBackup($formattedParent, $backupId, $formattedSourceBackup, $expireTime); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/CopyBackup', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $actualValue = $actualApiRequestObject->getBackupId(); - $this->assertProtobufEquals($backupId, $actualValue); - $actualValue = $actualApiRequestObject->getSourceBackup(); - $this->assertProtobufEquals($formattedSourceBackup, $actualValue); - $actualValue = $actualApiRequestObject->getExpireTime(); - $this->assertProtobufEquals($expireTime, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/copyBackupTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function copyBackupExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/copyBackupTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $backupId = 'backupId1355353272'; - $formattedSourceBackup = $gapicClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - $expireTime = new Timestamp(); - $response = $gapicClient->copyBackup($formattedParent, $backupId, $formattedSourceBackup, $expireTime); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/copyBackupTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createBackupTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createBackupTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $database = 'database1789464955'; - $name = 'name3373707'; - $sizeBytes = 1796325715; - $freeableSizeBytes = 1302251206; - $exclusiveSizeBytes = 1085921554; - $incrementalBackupChainId = 'incrementalBackupChainId-792099119'; - $expectedResponse = new Backup(); - $expectedResponse->setDatabase($database); - $expectedResponse->setName($name); - $expectedResponse->setSizeBytes($sizeBytes); - $expectedResponse->setFreeableSizeBytes($freeableSizeBytes); - $expectedResponse->setExclusiveSizeBytes($exclusiveSizeBytes); - $expectedResponse->setIncrementalBackupChainId($incrementalBackupChainId); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/createBackupTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $backupId = 'backupId1355353272'; - $backup = new Backup(); - $response = $gapicClient->createBackup($formattedParent, $backupId, $backup); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/CreateBackup', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $actualValue = $actualApiRequestObject->getBackupId(); - $this->assertProtobufEquals($backupId, $actualValue); - $actualValue = $actualApiRequestObject->getBackup(); - $this->assertProtobufEquals($backup, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createBackupTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createBackupExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createBackupTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $backupId = 'backupId1355353272'; - $backup = new Backup(); - $response = $gapicClient->createBackup($formattedParent, $backupId, $backup); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createBackupTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createBackupScheduleTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name = 'name3373707'; - $expectedResponse = new BackupSchedule(); - $expectedResponse->setName($name); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $backupScheduleId = 'backupScheduleId326010054'; - $backupSchedule = new BackupSchedule(); - $response = $gapicClient->createBackupSchedule($formattedParent, $backupScheduleId, $backupSchedule); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/CreateBackupSchedule', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $actualValue = $actualRequestObject->getBackupScheduleId(); - $this->assertProtobufEquals($backupScheduleId, $actualValue); - $actualValue = $actualRequestObject->getBackupSchedule(); - $this->assertProtobufEquals($backupSchedule, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function createBackupScheduleExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $backupScheduleId = 'backupScheduleId326010054'; - $backupSchedule = new BackupSchedule(); - try { - $gapicClient->createBackupSchedule($formattedParent, $backupScheduleId, $backupSchedule); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function createDatabaseTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createDatabaseTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $versionRetentionPeriod = 'versionRetentionPeriod907249289'; - $defaultLeader = 'defaultLeader1941180615'; - $enableDropProtection = false; - $reconciling = false; - $expectedResponse = new Database(); - $expectedResponse->setName($name); - $expectedResponse->setVersionRetentionPeriod($versionRetentionPeriod); - $expectedResponse->setDefaultLeader($defaultLeader); - $expectedResponse->setEnableDropProtection($enableDropProtection); - $expectedResponse->setReconciling($reconciling); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/createDatabaseTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $createStatement = 'createStatement552974828'; - $response = $gapicClient->createDatabase($formattedParent, $createStatement); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/CreateDatabase', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $actualValue = $actualApiRequestObject->getCreateStatement(); - $this->assertProtobufEquals($createStatement, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createDatabaseTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createDatabaseExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createDatabaseTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $createStatement = 'createStatement552974828'; - $response = $gapicClient->createDatabase($formattedParent, $createStatement); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createDatabaseTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function deleteBackupTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new GPBEmpty(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - $gapicClient->deleteBackup($formattedName); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/DeleteBackup', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteBackupExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - try { - $gapicClient->deleteBackup($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteBackupScheduleTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new GPBEmpty(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->backupScheduleName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SCHEDULE]'); - $gapicClient->deleteBackupSchedule($formattedName); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/DeleteBackupSchedule', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteBackupScheduleExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->backupScheduleName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SCHEDULE]'); - try { - $gapicClient->deleteBackupSchedule($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function dropDatabaseTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new GPBEmpty(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $gapicClient->dropDatabase($formattedDatabase); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/DropDatabase', $actualFuncCall); - $actualValue = $actualRequestObject->getDatabase(); - $this->assertProtobufEquals($formattedDatabase, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function dropDatabaseExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - try { - $gapicClient->dropDatabase($formattedDatabase); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getBackupTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $database = 'database1789464955'; - $name2 = 'name2-1052831874'; - $sizeBytes = 1796325715; - $freeableSizeBytes = 1302251206; - $exclusiveSizeBytes = 1085921554; - $incrementalBackupChainId = 'incrementalBackupChainId-792099119'; - $expectedResponse = new Backup(); - $expectedResponse->setDatabase($database); - $expectedResponse->setName($name2); - $expectedResponse->setSizeBytes($sizeBytes); - $expectedResponse->setFreeableSizeBytes($freeableSizeBytes); - $expectedResponse->setExclusiveSizeBytes($exclusiveSizeBytes); - $expectedResponse->setIncrementalBackupChainId($incrementalBackupChainId); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - $response = $gapicClient->getBackup($formattedName); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/GetBackup', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getBackupExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->backupName('[PROJECT]', '[INSTANCE]', '[BACKUP]'); - try { - $gapicClient->getBackup($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getBackupScheduleTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name2 = 'name2-1052831874'; - $expectedResponse = new BackupSchedule(); - $expectedResponse->setName($name2); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->backupScheduleName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SCHEDULE]'); - $response = $gapicClient->getBackupSchedule($formattedName); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/GetBackupSchedule', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getBackupScheduleExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->backupScheduleName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SCHEDULE]'); - try { - $gapicClient->getBackupSchedule($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getDatabaseTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name2 = 'name2-1052831874'; - $versionRetentionPeriod = 'versionRetentionPeriod907249289'; - $defaultLeader = 'defaultLeader1941180615'; - $enableDropProtection = false; - $reconciling = false; - $expectedResponse = new Database(); - $expectedResponse->setName($name2); - $expectedResponse->setVersionRetentionPeriod($versionRetentionPeriod); - $expectedResponse->setDefaultLeader($defaultLeader); - $expectedResponse->setEnableDropProtection($enableDropProtection); - $expectedResponse->setReconciling($reconciling); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $response = $gapicClient->getDatabase($formattedName); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/GetDatabase', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getDatabaseExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - try { - $gapicClient->getDatabase($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getDatabaseDdlTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $protoDescriptors = '13'; - $expectedResponse = new GetDatabaseDdlResponse(); - $expectedResponse->setProtoDescriptors($protoDescriptors); - $transport->addResponse($expectedResponse); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $response = $gapicClient->getDatabaseDdl($formattedDatabase); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/GetDatabaseDdl', $actualFuncCall); - $actualValue = $actualRequestObject->getDatabase(); - $this->assertProtobufEquals($formattedDatabase, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getDatabaseDdlExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - try { - $gapicClient->getDatabaseDdl($formattedDatabase); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getIamPolicyTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $version = 351608024; - $etag = '21'; - $expectedResponse = new Policy(); - $expectedResponse->setVersion($version); - $expectedResponse->setEtag($etag); - $transport->addResponse($expectedResponse); - // Mock request - $resource = 'resource-341064690'; - $response = $gapicClient->getIamPolicy($resource); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/GetIamPolicy', $actualFuncCall); - $actualValue = $actualRequestObject->getResource(); - $this->assertProtobufEquals($resource, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getIamPolicyExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $resource = 'resource-341064690'; - try { - $gapicClient->getIamPolicy($resource); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function internalUpdateGraphOperationTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new InternalUpdateGraphOperationResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $operationId = 'operationId-274116877'; - $vmIdentityToken = 'vmIdentityToken-1589919296'; - $response = $gapicClient->internalUpdateGraphOperation($formattedDatabase, $operationId, $vmIdentityToken); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/InternalUpdateGraphOperation', $actualFuncCall); - $actualValue = $actualRequestObject->getDatabase(); - $this->assertProtobufEquals($formattedDatabase, $actualValue); - $actualValue = $actualRequestObject->getOperationId(); - $this->assertProtobufEquals($operationId, $actualValue); - $actualValue = $actualRequestObject->getVmIdentityToken(); - $this->assertProtobufEquals($vmIdentityToken, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function internalUpdateGraphOperationExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $operationId = 'operationId-274116877'; - $vmIdentityToken = 'vmIdentityToken-1589919296'; - try { - $gapicClient->internalUpdateGraphOperation($formattedDatabase, $operationId, $vmIdentityToken); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listBackupOperationsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $operationsElement = new Operation(); - $operations = [ - $operationsElement, - ]; - $expectedResponse = new ListBackupOperationsResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setOperations($operations); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $response = $gapicClient->listBackupOperations($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getOperations()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/ListBackupOperations', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listBackupOperationsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - try { - $gapicClient->listBackupOperations($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listBackupSchedulesTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $backupSchedulesElement = new BackupSchedule(); - $backupSchedules = [ - $backupSchedulesElement, - ]; - $expectedResponse = new ListBackupSchedulesResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setBackupSchedules($backupSchedules); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $response = $gapicClient->listBackupSchedules($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getBackupSchedules()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/ListBackupSchedules', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listBackupSchedulesExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - try { - $gapicClient->listBackupSchedules($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listBackupsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $backupsElement = new Backup(); - $backups = [ - $backupsElement, - ]; - $expectedResponse = new ListBackupsResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setBackups($backups); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $response = $gapicClient->listBackups($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getBackups()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/ListBackups', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listBackupsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - try { - $gapicClient->listBackups($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listDatabaseOperationsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $operationsElement = new Operation(); - $operations = [ - $operationsElement, - ]; - $expectedResponse = new ListDatabaseOperationsResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setOperations($operations); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $response = $gapicClient->listDatabaseOperations($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getOperations()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/ListDatabaseOperations', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listDatabaseOperationsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - try { - $gapicClient->listDatabaseOperations($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listDatabaseRolesTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $databaseRolesElement = new DatabaseRole(); - $databaseRoles = [ - $databaseRolesElement, - ]; - $expectedResponse = new ListDatabaseRolesResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setDatabaseRoles($databaseRoles); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $response = $gapicClient->listDatabaseRoles($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getDatabaseRoles()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/ListDatabaseRoles', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listDatabaseRolesExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - try { - $gapicClient->listDatabaseRoles($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listDatabasesTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $databasesElement = new Database(); - $databases = [ - $databasesElement, - ]; - $expectedResponse = new ListDatabasesResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setDatabases($databases); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $response = $gapicClient->listDatabases($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getDatabases()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/ListDatabases', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listDatabasesExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - try { - $gapicClient->listDatabases($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function restoreDatabaseTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/restoreDatabaseTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $versionRetentionPeriod = 'versionRetentionPeriod907249289'; - $defaultLeader = 'defaultLeader1941180615'; - $enableDropProtection = false; - $reconciling = false; - $expectedResponse = new Database(); - $expectedResponse->setName($name); - $expectedResponse->setVersionRetentionPeriod($versionRetentionPeriod); - $expectedResponse->setDefaultLeader($defaultLeader); - $expectedResponse->setEnableDropProtection($enableDropProtection); - $expectedResponse->setReconciling($reconciling); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/restoreDatabaseTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $databaseId = 'databaseId816491103'; - $response = $gapicClient->restoreDatabase($formattedParent, $databaseId); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/RestoreDatabase', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $actualValue = $actualApiRequestObject->getDatabaseId(); - $this->assertProtobufEquals($databaseId, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/restoreDatabaseTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function restoreDatabaseExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/restoreDatabaseTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $databaseId = 'databaseId816491103'; - $response = $gapicClient->restoreDatabase($formattedParent, $databaseId); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/restoreDatabaseTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function setIamPolicyTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $version = 351608024; - $etag = '21'; - $expectedResponse = new Policy(); - $expectedResponse->setVersion($version); - $expectedResponse->setEtag($etag); - $transport->addResponse($expectedResponse); - // Mock request - $resource = 'resource-341064690'; - $policy = new Policy(); - $response = $gapicClient->setIamPolicy($resource, $policy); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/SetIamPolicy', $actualFuncCall); - $actualValue = $actualRequestObject->getResource(); - $this->assertProtobufEquals($resource, $actualValue); - $actualValue = $actualRequestObject->getPolicy(); - $this->assertProtobufEquals($policy, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function setIamPolicyExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $resource = 'resource-341064690'; - $policy = new Policy(); - try { - $gapicClient->setIamPolicy($resource, $policy); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function testIamPermissionsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new TestIamPermissionsResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $resource = 'resource-341064690'; - $permissions = []; - $response = $gapicClient->testIamPermissions($resource, $permissions); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/TestIamPermissions', $actualFuncCall); - $actualValue = $actualRequestObject->getResource(); - $this->assertProtobufEquals($resource, $actualValue); - $actualValue = $actualRequestObject->getPermissions(); - $this->assertProtobufEquals($permissions, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function testIamPermissionsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $resource = 'resource-341064690'; - $permissions = []; - try { - $gapicClient->testIamPermissions($resource, $permissions); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function updateBackupTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $database = 'database1789464955'; - $name = 'name3373707'; - $sizeBytes = 1796325715; - $freeableSizeBytes = 1302251206; - $exclusiveSizeBytes = 1085921554; - $incrementalBackupChainId = 'incrementalBackupChainId-792099119'; - $expectedResponse = new Backup(); - $expectedResponse->setDatabase($database); - $expectedResponse->setName($name); - $expectedResponse->setSizeBytes($sizeBytes); - $expectedResponse->setFreeableSizeBytes($freeableSizeBytes); - $expectedResponse->setExclusiveSizeBytes($exclusiveSizeBytes); - $expectedResponse->setIncrementalBackupChainId($incrementalBackupChainId); - $transport->addResponse($expectedResponse); - // Mock request - $backup = new Backup(); - $updateMask = new FieldMask(); - $response = $gapicClient->updateBackup($backup, $updateMask); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/UpdateBackup', $actualFuncCall); - $actualValue = $actualRequestObject->getBackup(); - $this->assertProtobufEquals($backup, $actualValue); - $actualValue = $actualRequestObject->getUpdateMask(); - $this->assertProtobufEquals($updateMask, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function updateBackupExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $backup = new Backup(); - $updateMask = new FieldMask(); - try { - $gapicClient->updateBackup($backup, $updateMask); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function updateBackupScheduleTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name = 'name3373707'; - $expectedResponse = new BackupSchedule(); - $expectedResponse->setName($name); - $transport->addResponse($expectedResponse); - // Mock request - $backupSchedule = new BackupSchedule(); - $updateMask = new FieldMask(); - $response = $gapicClient->updateBackupSchedule($backupSchedule, $updateMask); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/UpdateBackupSchedule', $actualFuncCall); - $actualValue = $actualRequestObject->getBackupSchedule(); - $this->assertProtobufEquals($backupSchedule, $actualValue); - $actualValue = $actualRequestObject->getUpdateMask(); - $this->assertProtobufEquals($updateMask, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function updateBackupScheduleExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $backupSchedule = new BackupSchedule(); - $updateMask = new FieldMask(); - try { - $gapicClient->updateBackupSchedule($backupSchedule, $updateMask); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function updateDatabaseTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateDatabaseTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $versionRetentionPeriod = 'versionRetentionPeriod907249289'; - $defaultLeader = 'defaultLeader1941180615'; - $enableDropProtection = false; - $reconciling = false; - $expectedResponse = new Database(); - $expectedResponse->setName($name); - $expectedResponse->setVersionRetentionPeriod($versionRetentionPeriod); - $expectedResponse->setDefaultLeader($defaultLeader); - $expectedResponse->setEnableDropProtection($enableDropProtection); - $expectedResponse->setReconciling($reconciling); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/updateDatabaseTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $database = new Database(); - $databaseName = 'databaseName-459093338'; - $database->setName($databaseName); - $updateMask = new FieldMask(); - $response = $gapicClient->updateDatabase($database, $updateMask); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/UpdateDatabase', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getDatabase(); - $this->assertProtobufEquals($database, $actualValue); - $actualValue = $actualApiRequestObject->getUpdateMask(); - $this->assertProtobufEquals($updateMask, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateDatabaseTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function updateDatabaseExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateDatabaseTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $database = new Database(); - $databaseName = 'databaseName-459093338'; - $database->setName($databaseName); - $updateMask = new FieldMask(); - $response = $gapicClient->updateDatabase($database, $updateMask); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateDatabaseTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function updateDatabaseDdlTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateDatabaseDdlTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $expectedResponse = new GPBEmpty(); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/updateDatabaseDdlTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $statements = []; - $response = $gapicClient->updateDatabaseDdl($formattedDatabase, $statements); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.database.v1.DatabaseAdmin/UpdateDatabaseDdl', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getDatabase(); - $this->assertProtobufEquals($formattedDatabase, $actualValue); - $actualValue = $actualApiRequestObject->getStatements(); - $this->assertProtobufEquals($statements, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateDatabaseDdlTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function updateDatabaseDdlExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateDatabaseDdlTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $statements = []; - $response = $gapicClient->updateDatabaseDdl($formattedDatabase, $statements); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateDatabaseDdlTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } -} diff --git a/Spanner/tests/Unit/Admin/Instance/V1/Client/InstanceAdminClientTest.php b/Spanner/tests/Unit/Admin/Instance/V1/Client/InstanceAdminClientTest.php index 612a5f58694f..c6f608c9865c 100644 --- a/Spanner/tests/Unit/Admin/Instance/V1/Client/InstanceAdminClientTest.php +++ b/Spanner/tests/Unit/Admin/Instance/V1/Client/InstanceAdminClientTest.php @@ -1,6 +1,6 @@ <?php /* - * Copyright 2023 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Spanner/tests/Unit/Admin/Instance/V1/InstanceAdminClientTest.php b/Spanner/tests/Unit/Admin/Instance/V1/InstanceAdminClientTest.php deleted file mode 100644 index f5f1a77996e6..000000000000 --- a/Spanner/tests/Unit/Admin/Instance/V1/InstanceAdminClientTest.php +++ /dev/null @@ -1,1955 +0,0 @@ -<?php -/* - * Copyright 2018 Google LLC - * - * 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 - * - * https://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. - */ - -/* - * GENERATED CODE WARNING - * This file was automatically generated - do not edit! - */ - -namespace Google\Cloud\Spanner\Admin\Instance\Tests\Unit\V1; - -use Google\ApiCore\ApiException; -use Google\ApiCore\CredentialsWrapper; -use Google\ApiCore\LongRunning\OperationsClient; -use Google\ApiCore\Testing\GeneratedTest; -use Google\ApiCore\Testing\MockTransport; -use Google\Cloud\Iam\V1\Policy; -use Google\Cloud\Iam\V1\TestIamPermissionsResponse; -use Google\Cloud\Spanner\Admin\Instance\V1\Instance; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; -use Google\Cloud\Spanner\Admin\Instance\V1\InstancePartition; -use Google\Cloud\Spanner\Admin\Instance\V1\ListInstanceConfigOperationsResponse; -use Google\Cloud\Spanner\Admin\Instance\V1\ListInstanceConfigsResponse; -use Google\Cloud\Spanner\Admin\Instance\V1\ListInstancePartitionOperationsResponse; -use Google\Cloud\Spanner\Admin\Instance\V1\ListInstancePartitionsResponse; -use Google\Cloud\Spanner\Admin\Instance\V1\ListInstancesResponse; -use Google\Cloud\Spanner\Admin\Instance\V1\MoveInstanceResponse; -use Google\LongRunning\GetOperationRequest; -use Google\LongRunning\Operation; -use Google\Protobuf\Any; -use Google\Protobuf\FieldMask; -use Google\Protobuf\GPBEmpty; -use Google\Rpc\Code; -use stdClass; - -/** - * @group instance - * - * @group gapic - */ -class InstanceAdminClientTest extends GeneratedTest -{ - /** @return TransportInterface */ - private function createTransport($deserialize = null) - { - return new MockTransport($deserialize); - } - - /** @return CredentialsWrapper */ - private function createCredentials() - { - return $this->getMockBuilder(CredentialsWrapper::class)->disableOriginalConstructor()->getMock(); - } - - /** @return InstanceAdminClient */ - private function createClient(array $options = []) - { - $options += [ - 'credentials' => $this->createCredentials(), - ]; - return new InstanceAdminClient($options); - } - - /** @test */ - public function createInstanceTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createInstanceTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $config = 'config-1354792126'; - $displayName = 'displayName1615086568'; - $nodeCount = 1539922066; - $processingUnits = 329117885; - $expectedResponse = new Instance(); - $expectedResponse->setName($name); - $expectedResponse->setConfig($config); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setNodeCount($nodeCount); - $expectedResponse->setProcessingUnits($processingUnits); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/createInstanceTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - $instanceId = 'instanceId-2101995259'; - $instance = new Instance(); - $instanceName = 'instanceName-737857344'; - $instance->setName($instanceName); - $instanceConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $instance->setConfig($instanceConfig); - $instanceDisplayName = 'instanceDisplayName1824500376'; - $instance->setDisplayName($instanceDisplayName); - $response = $gapicClient->createInstance($formattedParent, $instanceId, $instance); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/CreateInstance', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $actualValue = $actualApiRequestObject->getInstanceId(); - $this->assertProtobufEquals($instanceId, $actualValue); - $actualValue = $actualApiRequestObject->getInstance(); - $this->assertProtobufEquals($instance, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createInstanceTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createInstanceExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createInstanceTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - $instanceId = 'instanceId-2101995259'; - $instance = new Instance(); - $instanceName = 'instanceName-737857344'; - $instance->setName($instanceName); - $instanceConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $instance->setConfig($instanceConfig); - $instanceDisplayName = 'instanceDisplayName1824500376'; - $instance->setDisplayName($instanceDisplayName); - $response = $gapicClient->createInstance($formattedParent, $instanceId, $instance); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createInstanceTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createInstanceConfigTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createInstanceConfigTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $displayName = 'displayName1615086568'; - $baseConfig = 'baseConfig1990483056'; - $etag = 'etag3123477'; - $reconciling = false; - $storageLimitPerProcessingUnit = 1769187130; - $expectedResponse = new InstanceConfig(); - $expectedResponse->setName($name); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setBaseConfig($baseConfig); - $expectedResponse->setEtag($etag); - $expectedResponse->setReconciling($reconciling); - $expectedResponse->setStorageLimitPerProcessingUnit($storageLimitPerProcessingUnit); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/createInstanceConfigTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - $instanceConfigId = 'instanceConfigId-1789417458'; - $instanceConfig = new InstanceConfig(); - $response = $gapicClient->createInstanceConfig($formattedParent, $instanceConfigId, $instanceConfig); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/CreateInstanceConfig', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $actualValue = $actualApiRequestObject->getInstanceConfigId(); - $this->assertProtobufEquals($instanceConfigId, $actualValue); - $actualValue = $actualApiRequestObject->getInstanceConfig(); - $this->assertProtobufEquals($instanceConfig, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createInstanceConfigTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createInstanceConfigExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createInstanceConfigTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - $instanceConfigId = 'instanceConfigId-1789417458'; - $instanceConfig = new InstanceConfig(); - $response = $gapicClient->createInstanceConfig($formattedParent, $instanceConfigId, $instanceConfig); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createInstanceConfigTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createInstancePartitionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createInstancePartitionTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $config = 'config-1354792126'; - $displayName = 'displayName1615086568'; - $nodeCount = 1539922066; - $etag = 'etag3123477'; - $expectedResponse = new InstancePartition(); - $expectedResponse->setName($name); - $expectedResponse->setConfig($config); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setNodeCount($nodeCount); - $expectedResponse->setEtag($etag); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/createInstancePartitionTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $instancePartitionId = 'instancePartitionId288435706'; - $instancePartition = new InstancePartition(); - $instancePartitionName = 'instancePartitionName1272312320'; - $instancePartition->setName($instancePartitionName); - $instancePartitionConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $instancePartition->setConfig($instancePartitionConfig); - $instancePartitionDisplayName = 'instancePartitionDisplayName1175388504'; - $instancePartition->setDisplayName($instancePartitionDisplayName); - $response = $gapicClient->createInstancePartition($formattedParent, $instancePartitionId, $instancePartition); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/CreateInstancePartition', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $actualValue = $actualApiRequestObject->getInstancePartitionId(); - $this->assertProtobufEquals($instancePartitionId, $actualValue); - $actualValue = $actualApiRequestObject->getInstancePartition(); - $this->assertProtobufEquals($instancePartition, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createInstancePartitionTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function createInstancePartitionExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/createInstancePartitionTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $instancePartitionId = 'instancePartitionId288435706'; - $instancePartition = new InstancePartition(); - $instancePartitionName = 'instancePartitionName1272312320'; - $instancePartition->setName($instancePartitionName); - $instancePartitionConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $instancePartition->setConfig($instancePartitionConfig); - $instancePartitionDisplayName = 'instancePartitionDisplayName1175388504'; - $instancePartition->setDisplayName($instancePartitionDisplayName); - $response = $gapicClient->createInstancePartition($formattedParent, $instancePartitionId, $instancePartition); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/createInstancePartitionTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function deleteInstanceTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new GPBEmpty(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $gapicClient->deleteInstance($formattedName); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/DeleteInstance', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteInstanceExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - try { - $gapicClient->deleteInstance($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteInstanceConfigTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new GPBEmpty(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $gapicClient->deleteInstanceConfig($formattedName); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/DeleteInstanceConfig', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteInstanceConfigExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - try { - $gapicClient->deleteInstanceConfig($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteInstancePartitionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new GPBEmpty(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->instancePartitionName('[PROJECT]', '[INSTANCE]', '[INSTANCE_PARTITION]'); - $gapicClient->deleteInstancePartition($formattedName); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/DeleteInstancePartition', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteInstancePartitionExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->instancePartitionName('[PROJECT]', '[INSTANCE]', '[INSTANCE_PARTITION]'); - try { - $gapicClient->deleteInstancePartition($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getIamPolicyTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $version = 351608024; - $etag = '21'; - $expectedResponse = new Policy(); - $expectedResponse->setVersion($version); - $expectedResponse->setEtag($etag); - $transport->addResponse($expectedResponse); - // Mock request - $resource = 'resource-341064690'; - $response = $gapicClient->getIamPolicy($resource); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/GetIamPolicy', $actualFuncCall); - $actualValue = $actualRequestObject->getResource(); - $this->assertProtobufEquals($resource, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getIamPolicyExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $resource = 'resource-341064690'; - try { - $gapicClient->getIamPolicy($resource); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getInstanceTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name2 = 'name2-1052831874'; - $config = 'config-1354792126'; - $displayName = 'displayName1615086568'; - $nodeCount = 1539922066; - $processingUnits = 329117885; - $expectedResponse = new Instance(); - $expectedResponse->setName($name2); - $expectedResponse->setConfig($config); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setNodeCount($nodeCount); - $expectedResponse->setProcessingUnits($processingUnits); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $response = $gapicClient->getInstance($formattedName); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/GetInstance', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getInstanceExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - try { - $gapicClient->getInstance($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getInstanceConfigTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name2 = 'name2-1052831874'; - $displayName = 'displayName1615086568'; - $baseConfig = 'baseConfig1990483056'; - $etag = 'etag3123477'; - $reconciling = false; - $storageLimitPerProcessingUnit = 1769187130; - $expectedResponse = new InstanceConfig(); - $expectedResponse->setName($name2); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setBaseConfig($baseConfig); - $expectedResponse->setEtag($etag); - $expectedResponse->setReconciling($reconciling); - $expectedResponse->setStorageLimitPerProcessingUnit($storageLimitPerProcessingUnit); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $response = $gapicClient->getInstanceConfig($formattedName); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/GetInstanceConfig', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getInstanceConfigExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - try { - $gapicClient->getInstanceConfig($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getInstancePartitionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name2 = 'name2-1052831874'; - $config = 'config-1354792126'; - $displayName = 'displayName1615086568'; - $nodeCount = 1539922066; - $etag = 'etag3123477'; - $expectedResponse = new InstancePartition(); - $expectedResponse->setName($name2); - $expectedResponse->setConfig($config); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setNodeCount($nodeCount); - $expectedResponse->setEtag($etag); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->instancePartitionName('[PROJECT]', '[INSTANCE]', '[INSTANCE_PARTITION]'); - $response = $gapicClient->getInstancePartition($formattedName); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/GetInstancePartition', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getInstancePartitionExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->instancePartitionName('[PROJECT]', '[INSTANCE]', '[INSTANCE_PARTITION]'); - try { - $gapicClient->getInstancePartition($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstanceConfigOperationsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $operationsElement = new Operation(); - $operations = [ - $operationsElement, - ]; - $expectedResponse = new ListInstanceConfigOperationsResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setOperations($operations); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - $response = $gapicClient->listInstanceConfigOperations($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getOperations()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/ListInstanceConfigOperations', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstanceConfigOperationsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - try { - $gapicClient->listInstanceConfigOperations($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstanceConfigsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $instanceConfigsElement = new InstanceConfig(); - $instanceConfigs = [ - $instanceConfigsElement, - ]; - $expectedResponse = new ListInstanceConfigsResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setInstanceConfigs($instanceConfigs); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - $response = $gapicClient->listInstanceConfigs($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getInstanceConfigs()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/ListInstanceConfigs', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstanceConfigsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - try { - $gapicClient->listInstanceConfigs($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstancePartitionOperationsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $operationsElement = new Operation(); - $operations = [ - $operationsElement, - ]; - $expectedResponse = new ListInstancePartitionOperationsResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setOperations($operations); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $response = $gapicClient->listInstancePartitionOperations($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getOperations()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/ListInstancePartitionOperations', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstancePartitionOperationsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - try { - $gapicClient->listInstancePartitionOperations($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstancePartitionsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $instancePartitionsElement = new InstancePartition(); - $instancePartitions = [ - $instancePartitionsElement, - ]; - $expectedResponse = new ListInstancePartitionsResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setInstancePartitions($instancePartitions); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $response = $gapicClient->listInstancePartitions($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getInstancePartitions()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/ListInstancePartitions', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstancePartitionsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - try { - $gapicClient->listInstancePartitions($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstancesTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $instancesElement = new Instance(); - $instances = [ - $instancesElement, - ]; - $expectedResponse = new ListInstancesResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setInstances($instances); - $transport->addResponse($expectedResponse); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - $response = $gapicClient->listInstances($formattedParent); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getInstances()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/ListInstances', $actualFuncCall); - $actualValue = $actualRequestObject->getParent(); - $this->assertProtobufEquals($formattedParent, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listInstancesExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedParent = $gapicClient->projectName('[PROJECT]'); - try { - $gapicClient->listInstances($formattedParent); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function moveInstanceTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/moveInstanceTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $expectedResponse = new MoveInstanceResponse(); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/moveInstanceTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $formattedName = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $formattedTargetConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $response = $gapicClient->moveInstance($formattedName, $formattedTargetConfig); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/MoveInstance', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $actualValue = $actualApiRequestObject->getTargetConfig(); - $this->assertProtobufEquals($formattedTargetConfig, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/moveInstanceTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function moveInstanceExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/moveInstanceTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->instanceName('[PROJECT]', '[INSTANCE]'); - $formattedTargetConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $response = $gapicClient->moveInstance($formattedName, $formattedTargetConfig); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/moveInstanceTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function setIamPolicyTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $version = 351608024; - $etag = '21'; - $expectedResponse = new Policy(); - $expectedResponse->setVersion($version); - $expectedResponse->setEtag($etag); - $transport->addResponse($expectedResponse); - // Mock request - $resource = 'resource-341064690'; - $policy = new Policy(); - $response = $gapicClient->setIamPolicy($resource, $policy); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/SetIamPolicy', $actualFuncCall); - $actualValue = $actualRequestObject->getResource(); - $this->assertProtobufEquals($resource, $actualValue); - $actualValue = $actualRequestObject->getPolicy(); - $this->assertProtobufEquals($policy, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function setIamPolicyExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $resource = 'resource-341064690'; - $policy = new Policy(); - try { - $gapicClient->setIamPolicy($resource, $policy); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function testIamPermissionsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new TestIamPermissionsResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $resource = 'resource-341064690'; - $permissions = []; - $response = $gapicClient->testIamPermissions($resource, $permissions); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/TestIamPermissions', $actualFuncCall); - $actualValue = $actualRequestObject->getResource(); - $this->assertProtobufEquals($resource, $actualValue); - $actualValue = $actualRequestObject->getPermissions(); - $this->assertProtobufEquals($permissions, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function testIamPermissionsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $resource = 'resource-341064690'; - $permissions = []; - try { - $gapicClient->testIamPermissions($resource, $permissions); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function updateInstanceTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateInstanceTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $config = 'config-1354792126'; - $displayName = 'displayName1615086568'; - $nodeCount = 1539922066; - $processingUnits = 329117885; - $expectedResponse = new Instance(); - $expectedResponse->setName($name); - $expectedResponse->setConfig($config); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setNodeCount($nodeCount); - $expectedResponse->setProcessingUnits($processingUnits); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/updateInstanceTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $instance = new Instance(); - $instanceName = 'instanceName-737857344'; - $instance->setName($instanceName); - $instanceConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $instance->setConfig($instanceConfig); - $instanceDisplayName = 'instanceDisplayName1824500376'; - $instance->setDisplayName($instanceDisplayName); - $fieldMask = new FieldMask(); - $response = $gapicClient->updateInstance($instance, $fieldMask); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/UpdateInstance', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getInstance(); - $this->assertProtobufEquals($instance, $actualValue); - $actualValue = $actualApiRequestObject->getFieldMask(); - $this->assertProtobufEquals($fieldMask, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateInstanceTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function updateInstanceExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateInstanceTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $instance = new Instance(); - $instanceName = 'instanceName-737857344'; - $instance->setName($instanceName); - $instanceConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $instance->setConfig($instanceConfig); - $instanceDisplayName = 'instanceDisplayName1824500376'; - $instance->setDisplayName($instanceDisplayName); - $fieldMask = new FieldMask(); - $response = $gapicClient->updateInstance($instance, $fieldMask); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateInstanceTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function updateInstanceConfigTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateInstanceConfigTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $displayName = 'displayName1615086568'; - $baseConfig = 'baseConfig1990483056'; - $etag = 'etag3123477'; - $reconciling = false; - $storageLimitPerProcessingUnit = 1769187130; - $expectedResponse = new InstanceConfig(); - $expectedResponse->setName($name); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setBaseConfig($baseConfig); - $expectedResponse->setEtag($etag); - $expectedResponse->setReconciling($reconciling); - $expectedResponse->setStorageLimitPerProcessingUnit($storageLimitPerProcessingUnit); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/updateInstanceConfigTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $instanceConfig = new InstanceConfig(); - $updateMask = new FieldMask(); - $response = $gapicClient->updateInstanceConfig($instanceConfig, $updateMask); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/UpdateInstanceConfig', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getInstanceConfig(); - $this->assertProtobufEquals($instanceConfig, $actualValue); - $actualValue = $actualApiRequestObject->getUpdateMask(); - $this->assertProtobufEquals($updateMask, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateInstanceConfigTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function updateInstanceConfigExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateInstanceConfigTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $instanceConfig = new InstanceConfig(); - $updateMask = new FieldMask(); - $response = $gapicClient->updateInstanceConfig($instanceConfig, $updateMask); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateInstanceConfigTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function updateInstancePartitionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateInstancePartitionTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $name = 'name3373707'; - $config = 'config-1354792126'; - $displayName = 'displayName1615086568'; - $nodeCount = 1539922066; - $etag = 'etag3123477'; - $expectedResponse = new InstancePartition(); - $expectedResponse->setName($name); - $expectedResponse->setConfig($config); - $expectedResponse->setDisplayName($displayName); - $expectedResponse->setNodeCount($nodeCount); - $expectedResponse->setEtag($etag); - $anyResponse = new Any(); - $anyResponse->setValue($expectedResponse->serializeToString()); - $completeOperation = new Operation(); - $completeOperation->setName('operations/updateInstancePartitionTest'); - $completeOperation->setDone(true); - $completeOperation->setResponse($anyResponse); - $operationsTransport->addResponse($completeOperation); - // Mock request - $instancePartition = new InstancePartition(); - $instancePartitionName = 'instancePartitionName1272312320'; - $instancePartition->setName($instancePartitionName); - $instancePartitionConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $instancePartition->setConfig($instancePartitionConfig); - $instancePartitionDisplayName = 'instancePartitionDisplayName1175388504'; - $instancePartition->setDisplayName($instancePartitionDisplayName); - $fieldMask = new FieldMask(); - $response = $gapicClient->updateInstancePartition($instancePartition, $fieldMask); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $apiRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($apiRequests)); - $operationsRequestsEmpty = $operationsTransport->popReceivedCalls(); - $this->assertSame(0, count($operationsRequestsEmpty)); - $actualApiFuncCall = $apiRequests[0]->getFuncCall(); - $actualApiRequestObject = $apiRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.admin.instance.v1.InstanceAdmin/UpdateInstancePartition', $actualApiFuncCall); - $actualValue = $actualApiRequestObject->getInstancePartition(); - $this->assertProtobufEquals($instancePartition, $actualValue); - $actualValue = $actualApiRequestObject->getFieldMask(); - $this->assertProtobufEquals($fieldMask, $actualValue); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateInstancePartitionTest'); - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - $this->assertTrue($response->isDone()); - $this->assertEquals($expectedResponse, $response->getResult()); - $apiRequestsEmpty = $transport->popReceivedCalls(); - $this->assertSame(0, count($apiRequestsEmpty)); - $operationsRequests = $operationsTransport->popReceivedCalls(); - $this->assertSame(1, count($operationsRequests)); - $actualOperationsFuncCall = $operationsRequests[0]->getFuncCall(); - $actualOperationsRequestObject = $operationsRequests[0]->getRequestObject(); - $this->assertSame('/google.longrunning.Operations/GetOperation', $actualOperationsFuncCall); - $this->assertEquals($expectedOperationsRequestObject, $actualOperationsRequestObject); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } - - /** @test */ - public function updateInstancePartitionExceptionTest() - { - $operationsTransport = $this->createTransport(); - $operationsClient = new OperationsClient([ - 'apiEndpoint' => '', - 'transport' => $operationsTransport, - 'credentials' => $this->createCredentials(), - ]); - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - 'operationsClient' => $operationsClient, - ]); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - // Mock response - $incompleteOperation = new Operation(); - $incompleteOperation->setName('operations/updateInstancePartitionTest'); - $incompleteOperation->setDone(false); - $transport->addResponse($incompleteOperation); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $operationsTransport->addResponse(null, $status); - // Mock request - $instancePartition = new InstancePartition(); - $instancePartitionName = 'instancePartitionName1272312320'; - $instancePartition->setName($instancePartitionName); - $instancePartitionConfig = $gapicClient->instanceConfigName('[PROJECT]', '[INSTANCE_CONFIG]'); - $instancePartition->setConfig($instancePartitionConfig); - $instancePartitionDisplayName = 'instancePartitionDisplayName1175388504'; - $instancePartition->setDisplayName($instancePartitionDisplayName); - $fieldMask = new FieldMask(); - $response = $gapicClient->updateInstancePartition($instancePartition, $fieldMask); - $this->assertFalse($response->isDone()); - $this->assertNull($response->getResult()); - $expectedOperationsRequestObject = new GetOperationRequest(); - $expectedOperationsRequestObject->setName('operations/updateInstancePartitionTest'); - try { - $response->pollUntilComplete([ - 'initialPollDelayMillis' => 1, - ]); - // If the pollUntilComplete() method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stubs are exhausted - $transport->popReceivedCalls(); - $operationsTransport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - $this->assertTrue($operationsTransport->isExhausted()); - } -} diff --git a/Spanner/tests/Unit/ArrayTypeTest.php b/Spanner/tests/Unit/ArrayTypeTest.php index d04b0f2d7fd2..23013ce50351 100644 --- a/Spanner/tests/Unit/ArrayTypeTest.php +++ b/Spanner/tests/Unit/ArrayTypeTest.php @@ -1,4 +1,5 @@ <?php + /** * Copyright 2018 Google Inc. * @@ -55,7 +56,7 @@ public function invalidTypeProvider() return [ ['hello'], [100], - [3.1415], + [-1], [Database::TYPE_ARRAY], [Database::TYPE_STRUCT] ]; @@ -72,7 +73,7 @@ public function testArrayType($type) public function testArrayTypeStruct() { - $struct = new StructType; + $struct = new StructType(); $struct->add('foo', Database::TYPE_STRING); $arr = new ArrayType($struct); diff --git a/Spanner/tests/Unit/BackupTest.php b/Spanner/tests/Unit/BackupTest.php index b02926a3155d..d9615f6ea9a0 100644 --- a/Spanner/tests/Unit/BackupTest.php +++ b/Spanner/tests/Unit/BackupTest.php @@ -17,22 +17,26 @@ namespace Google\Cloud\Spanner\Tests\Unit; -use Google\ApiCore\ApiException; +use DateTime; +use DMS\PHPUnitExtensions\ArraySubset\ArraySubsetAsserts; use Google\ApiCore\OperationResponse; -use Google\Cloud\Core\Exception\NotFoundException; -use Google\Cloud\Core\Iam\Iam; -use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Backup as BackupProto; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\CopyBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\CreateBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\DeleteBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\GetBackupRequest; +use Google\Cloud\Spanner\Admin\Database\V1\UpdateBackupRequest; use Google\Cloud\Spanner\Backup; -use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; -use Google\Cloud\Spanner\Timestamp; +use Google\Cloud\Spanner\Serializer; +use Google\LongRunning\Client\OperationsClient; +use Google\Protobuf\FieldMask; +use Google\Protobuf\Timestamp; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -45,6 +49,8 @@ class BackupTest extends TestCase { use GrpcTestTrait; use ProphecyTrait; + use ArraySubsetAsserts; + use ApiHelperTrait; const PROJECT_ID = 'test-project'; const INSTANCE = 'instance-name'; @@ -52,202 +58,294 @@ class BackupTest extends TestCase const BACKUP = 'backup-name'; const COPIED_BACKUP = 'new-backup-name'; - private $connection; + private $databaseAdminClient; + private Serializer $serializer; private $instance; + private $operationResponse; + private $database; - private $lro; - private $lroCallables; - private $expireTime; - private $createTime; + private DateTime $expireTime; private $versionTime; - private $backup; - private $copiedBackup; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->prophesize(ConnectionInterface::class); - $this->instance = $this->prophesize(Instance::class); + $this->databaseAdminClient = $this->prophesize(DatabaseAdminClient::class); + $this->databaseAdminClient->getOperationsClient() + ->willReturn($this->prophesize(OperationsClient::class)->reveal()); + $this->serializer = new Serializer(); + $this->database = $this->prophesize(Database::class); $this->database->name()->willReturn( DatabaseAdminClient::databaseName(self::PROJECT_ID, self::INSTANCE, self::DATABASE) ); - $this->instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT_ID, self::INSTANCE)); + + $this->instance = $this->prophesize(Instance::class); + $this->instance->name()->willReturn(DatabaseAdminClient::instanceName(self::PROJECT_ID, self::INSTANCE)); $this->instance->database(Argument::any())->willReturn($this->database); - $this->lro = $this->prophesize(LongRunningConnectionInterface::class); - $this->lroCallables = []; - $this->expireTime = new \DateTime("+ 7 hours"); - $this->createTime = $this->expireTime; - $this->versionTime = new \DateTime("- 2 hours"); - - $args=[ - $this->connection->reveal(), - $this->instance->reveal(), - $this->lro->reveal(), - $this->lroCallables, - self::PROJECT_ID, - self::BACKUP - ]; - $props = [ - 'instance', 'connection' - ]; - $this->backup = TestHelpers::stub(Backup::class, $args, $props); - // copiedBackup will contain a mock of the backup object where - // $backup will be copied into - $copyArgs = $args; - $copyArgs[5] = self::COPIED_BACKUP; - $this->copiedBackup = TestHelpers::stub(Backup::class, $copyArgs, $props); + $this->operationResponse = $this->prophesize(OperationResponse::class); + + $this->expireTime = new DateTime('+7 hours'); + $this->versionTime = new DateTime('-2 hours'); } public function testName() { + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP + ); + $this->assertEquals( DatabaseAdminClient::backupName(self::PROJECT_ID, self::INSTANCE, self::BACKUP), - $this->backup->name() + $backup->name() ); } public function testCreate() { - $this->connection->createBackup(Argument::allOf( - Argument::withEntry('instance', InstanceAdminClient::instanceName(self::PROJECT_ID, self::INSTANCE)), - Argument::withEntry('backupId', self::BACKUP), - Argument::withEntry('backup', [ - 'database' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::INSTANCE, self::DATABASE), - 'expireTime' => $this->expireTime->format('Y-m-d\TH:i:s.u\Z'), - ]), - Argument::withEntry('versionTime', $this->versionTime->format('Y-m-d\TH:i:s.u\Z')) - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->backup->___setProperty('connection', $this->connection->reveal()); - $op = $this->backup->create(self::DATABASE, $this->expireTime, [ + $expected = [ + 'parent' => DatabaseAdminClient::instanceName(self::PROJECT_ID, self::INSTANCE), + 'database' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::INSTANCE, self::DATABASE), + 'expire_time' => $this->expireTime->format('U'), + 'version_time' => $this->versionTime->format('U'), + ]; + $this->databaseAdminClient->createBackup( + Argument::that(function (CreateBackupRequest $request) use ($expected) { + $this->assertEquals($expected['parent'], $request->getParent()); + $this->assertEquals(self::BACKUP, $request->getBackupId()); + $this->assertEquals($expected['database'], $request->getBackup()->getDatabase()); + $this->assertEquals($expected['expire_time'], $request->getBackup()->getExpireTime()->getSeconds()); + $this->assertEquals($expected['version_time'], $request->getBackup()->getVersionTime()->getSeconds()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); + + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP + ); + + $operation = $backup->create(self::DATABASE, $this->expireTime, [ 'versionTime' => $this->versionTime, ]); - $this->assertInstanceOf(LongRunningOperation::class, $op); + $this->assertInstanceOf(LongRunningOperation::class, $operation); } public function testCreateCopy() { - $this->connection->copyBackup(Argument::allOf( - Argument::withEntry('instance', InstanceAdminClient::instanceName(self::PROJECT_ID, self::INSTANCE)), - Argument::withEntry('backupId', self::COPIED_BACKUP), - Argument::withKey('sourceBackupId'), - Argument::withEntry('expireTime', $this->expireTime->format('Y-m-d\TH:i:s.u\Z')) - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->backup->___setProperty('connection', $this->connection->reveal()); - $op = $this->backup->createCopy($this->copiedBackup, $this->expireTime); + $expected = [ + 'parent' => DatabaseAdminClient::instanceName(self::PROJECT_ID, self::INSTANCE), + 'source_backup' => DatabaseAdminClient::backupName(self::PROJECT_ID, self::INSTANCE, self::BACKUP), + 'expire_time' => $this->expireTime->format('U'), + ]; + $this->databaseAdminClient->copyBackup( + Argument::that(function (CopyBackupRequest $request) use ($expected) { + $this->assertEquals($expected['parent'], $request->getParent()); + $this->assertEquals(self::COPIED_BACKUP, $request->getBackupId()); + $this->assertEquals($expected['source_backup'], $request->getSourceBackup()); + $this->assertEquals($expected['expire_time'], $request->getExpireTime()->getSeconds()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); + + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP + ); + + $copiedBackup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::COPIED_BACKUP + ); + + $op = $backup->createCopy($copiedBackup, $this->expireTime); $this->assertInstanceOf(LongRunningOperation::class, $op); } public function testDelete() { - $this->connection->deleteBackup(Argument::withEntry('name', $this->backup->name())) - ->shouldBeCalled(); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->deleteBackup( + Argument::type(DeleteBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce(); + + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP + ); - $this->backup->delete(); + $backup->delete(); } public function testInfo() { - $res = [ - 'name' => $this->backup->name(), - 'expireTime' => $this->expireTime->format('Y-m-d\TH:i:s.u\Z'), - 'createTime' => $this->createTime->format('Y-m-d\TH:i:s.u\Z'), - 'versionTime' => $this->versionTime->format('Y-m-d\TH:i:s.u\Z') - ]; - - $this->connection->getBackup(Argument::withEntry('name', $this->backup->name())) - ->shouldBeCalledTimes(1) - ->willReturn($res); + $response = new BackupProto([ + 'name' => DatabaseAdminClient::backupName(self::PROJECT_ID, self::INSTANCE, self::BACKUP), + 'expire_time' => new Timestamp(['seconds' => $this->expireTime->format('U')]), + 'create_time' => new Timestamp(['seconds' => $this->expireTime->format('U')]), + 'version_time' => new Timestamp(['seconds' => $this->versionTime->format('U')]), + ]); - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getBackup( + Argument::type(GetBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($response); + + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP + ); - $info = $this->backup->info(); + $info = $backup->info(); - $this->assertEquals($res, $info); + $this->assertArraySubset([ + 'name' => $response->getName(), + 'expireTime' => $this->expireTime->format('Y-m-d\TH:i:s.000000\Z'), + 'createTime' => $this->expireTime->format('Y-m-d\TH:i:s.000000\Z'), + 'versionTime' => $this->versionTime->format('Y-m-d\TH:i:s.000000\Z'), + ], $info); // Make sure the request only is sent once. - $this->backup->info(); + $backup->info(); } public function testReload() { - $res = [ - 'name' => $this->backup->name(), - 'expireTime' => $this->expireTime->format('Y-m-d\TH:i:s.u\Z'), - 'createTime' => $this->createTime->format('Y-m-d\TH:i:s.u\Z'), - 'versionTime' => $this->versionTime->format('Y-m-d\TH:i:s.u\Z') - ]; - - $this->connection->getBackup(Argument::withEntry('name', $this->backup->name())) - ->shouldBeCalled() - ->willReturn($res); + $response = new BackupProto([ + 'name' => DatabaseAdminClient::backupName(self::PROJECT_ID, self::INSTANCE, self::BACKUP), + ]); - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getBackup( + Argument::type(GetBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($response); + + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP, + ['backup' => ['name' => 'different-name']] + ); - $info = $this->backup->reload(); + $info = $backup->reload(); - $this->assertEquals($res, $info); + $this->assertArraySubset([ + 'name' => $response->getName(), + ], $info); } public function testState() { - $res = [ - 'state' => Backup::STATE_READY - ]; - $this->connection->getBackup(Argument::withEntry('name', $this->backup->name())) - ->shouldBeCalledTimes(1) - ->willReturn($res); + $response = new BackupProto([ + 'state' => Backup::STATE_READY, + ]); - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getBackup( + Argument::type(GetBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($response); + + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP + ); - $this->assertEquals(Backup::STATE_READY, $this->backup->state()); + $this->assertEquals(Backup::STATE_READY, $backup->state()); // Make sure the request only is sent once. - $this->backup->state(); + $backup->state(); } public function testExists() { - $this->connection->getBackup(Argument::withEntry('name', $this->backup->name())) - ->shouldBeCalled() - ->willReturn([]); - - $this->backup->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getBackup( + Argument::type(GetBackupRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new BackupProto()); + + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP + ); - $this->assertTrue($this->backup->exists()); + $this->assertTrue($backup->exists()); } public function testUpdateExpireTime() { - $res = ['name' => 'foo', 'expireTime' => $this->expireTime->format('Y-m-d\TH:i:s.u\Z')]; - - $this->connection->updateBackup(Argument::allOf( - Argument::withEntry('backup', [ - 'name' => $this->backup->name(), - 'expireTime' => $this->expireTime->format('Y-m-d\TH:i:s.u\Z') - ]), - Argument::withEntry('updateMask', ['paths' => ['expire_time']]) - )) - ->shouldBeCalled() - ->willReturn($res); - - $this->backup->___setProperty('connection', $this->connection->reveal()); - - $info = $this->backup->updateExpireTime($this->expireTime); - $this->assertEquals($res, $info); + $newExpireTime = new DateTime('+1 day'); + + $response = new BackupProto([ + 'name' => 'foo', + 'expire_time' => new Timestamp(['seconds' => $newExpireTime->format('U')]), + ]); + + $this->databaseAdminClient->updateBackup( + Argument::that(function (UpdateBackupRequest $request) use ($newExpireTime) { + $this->assertEquals(new FieldMask(['paths' => ['expire_time']]), $request->getUpdateMask()); + $this->assertEquals($newExpireTime->format('U'), $request->getBackup()->getExpireTime()->getSeconds()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($response); + + $backup = new Backup( + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance->reveal(), + self::PROJECT_ID, + self::BACKUP + ); + + $info = $backup->updateExpireTime($newExpireTime); + $this->assertArraySubset([ + 'expireTime' => $newExpireTime->format('Y-m-d\TH:i:s.000000\Z'), + ], $info); } } diff --git a/Spanner/tests/Unit/Batch/BatchClientTest.php b/Spanner/tests/Unit/Batch/BatchClientTest.php index 3cb3b0b262f6..8cb2cdc1e9e8 100644 --- a/Spanner/tests/Unit/Batch/BatchClientTest.php +++ b/Spanner/tests/Unit/Batch/BatchClientTest.php @@ -17,7 +17,7 @@ namespace Google\Cloud\Spanner\Tests\Unit\Batch; -use Google\Cloud\Core\Testing\TestHelpers; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\Timestamp; use Google\Cloud\Core\TimeTrait; use Google\Cloud\Spanner\Batch\BatchClient; @@ -26,13 +26,17 @@ use Google\Cloud\Spanner\Batch\ReadPartition; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; +use Google\Cloud\Spanner\Serializer; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\Session; +use Google\Cloud\Spanner\V1\Transaction; +use Google\Protobuf\Timestamp as TimestampProto; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use Google\Cloud\Spanner\SpannerClient; /** * @group spanner @@ -41,56 +45,59 @@ */ class BatchClientTest extends TestCase { - use OperationRefreshTrait; use ProphecyTrait; - use StubCreationTrait; use TimeTrait; + use ApiHelperTrait; const DATABASE = 'projects/my-awesome-project/instances/my-instance/databases/my-database'; const SESSION = 'projects/my-awesome-project/instances/my-instance/databases/my-database/sessions/session-id'; const TRANSACTION = 'transaction-id'; - private $connection; - private $client; + private $spannerClient; + private $serializer; + private $batchClient; public function setUp(): void { - $this->connection = $this->getConnStub(); - $this->client = TestHelpers::stub(BatchClient::class, [ - new Operation($this->connection->reveal(), false), + $this->serializer = new Serializer(); + $this->spannerClient = $this->prophesize(GapicSpannerClient::class); + $this->batchClient = new BatchClient( + new Operation($this->spannerClient->reveal(), $this->serializer), self::DATABASE - ], [ - 'operation' - ]); + ); } public function testSnapshot() { $time = time(); + $this->spannerClient->createSession( + Argument::that(function (CreateSessionRequest $request) { + $this->assertEquals( + $request->getDatabase(), + self::DATABASE + ); + return true; + }), + Argument::type('array') + )->shouldBeCalledOnce()->willReturn(new Session(['name' => self::SESSION])); + + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertEquals( + $this->serializer->encodeMessage($request)['options']['readOnly'], + ['returnReadTimestamp' => true] + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new Transaction([ + 'id' => self::TRANSACTION, + 'read_timestamp' => new TimestampProto(['seconds' => $time]) + ])); - $this->connection->createSession(Argument::withEntry('database', self::DATABASE)) - ->shouldBeCalledTimes(1) - ->willReturn([ - 'name' => self::SESSION - ]); - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('singleUse', false), - Argument::withEntry('session', self::SESSION), - Argument::that(function (array $args) { - if ($args['transactionOptions']['readOnly']['returnReadTimestamp'] !== true) { - return false; - } - - return $args['database'] === self::DATABASE; - }) - ))->shouldBeCalled()->willReturn([ - 'id' => self::TRANSACTION, - 'readTimestamp' => \DateTime::createFromFormat('U', (string) $time)->format(Timestamp::FORMAT) - ]); - - $this->refreshOperation($this->client, $this->connection->reveal()); - - $snapshot = $this->client->snapshot(); + $snapshot = $this->batchClient->snapshot(); $this->assertInstanceOf(BatchSnapshot::class, $snapshot); } @@ -104,7 +111,7 @@ public function testSnapshotFromString() 'readTimestamp' => \DateTime::createFromFormat('U', (string) $time)->format(Timestamp::FORMAT) ])); - $snapshot = $this->client->snapshotFromString($identifier); + $snapshot = $this->batchClient->snapshotFromString($identifier); $this->assertEquals(self::SESSION, $snapshot->session()->name()); $this->assertEquals(self::TRANSACTION, $snapshot->id()); $this->assertEquals( @@ -122,7 +129,7 @@ public function testQueryPartitionFromString() $partition = new QueryPartition($token, $sql, $options); $string = (string) $partition; - $res = $this->client->partitionFromString($partition); + $res = $this->batchClient->partitionFromString($partition); $this->assertEquals($token, $res->token()); $this->assertEquals($sql, $res->sql()); $this->assertEquals($options, $res->options()); @@ -133,13 +140,13 @@ public function testReadPartitionFromString() $token = 'foobar'; $table = 'table'; $keyset = new KeySet(['all' => true]); - $columns = ['a','b']; + $columns = ['a', 'b']; $options = ['hello' => 'world']; $partition = new ReadPartition($token, $table, $keyset, $columns, $options); $string = (string) $partition; - $res = $this->client->partitionFromString($partition); + $res = $this->batchClient->partitionFromString($partition); $this->assertEquals($token, $res->token()); $this->assertEquals($table, $res->table()); $this->assertEquals($keyset->keySetObject(), $res->keySet()->keySetObject()); @@ -153,7 +160,7 @@ public function testMissingPartitionTypeKey() $this->expectExceptionMessage('Invalid partition data.'); $data = base64_encode(json_encode(['hello' => 'world'])); - $this->client->partitionFromString($data); + $this->batchClient->partitionFromString($data); } public function testInvalidPartitionType() @@ -162,36 +169,43 @@ public function testInvalidPartitionType() $this->expectExceptionMessage('Invalid partition type.'); $data = base64_encode(json_encode([BatchClient::PARTITION_TYPE_KEY => uniqid('this-is-not-real')])); - $this->client->partitionFromString($data); + $this->batchClient->partitionFromString($data); } public function testSnapshotDatabaseRole() { $time = time(); + $this->spannerClient->createSession( + Argument::that(function (CreateSessionRequest $request) { + return $this->serializer->encodeMessage($request)['session']['creatorRole'] == 'Reader'; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new Session(['name' => self::SESSION])); + + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertEquals( + $this->serializer->encodeMessage($request)['options']['readOnly'], + ['returnReadTimestamp' => true] + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new Transaction([ + 'id' => self::TRANSACTION, + 'read_timestamp' => new TimestampProto(['seconds' => $time]) + ])); - $client = TestHelpers::stub(BatchClient::class, [ - new Operation($this->connection->reveal(), false), + $batchClient = new BatchClient( + new Operation($this->spannerClient->reveal(), $this->serializer), self::DATABASE, ['databaseRole' => 'Reader'] - ], [ - 'operation' - ]); + ); - $this->connection->beginTransaction(Argument::any()) - ->shouldBeCalled()->willReturn([ - 'id' => self::TRANSACTION, - 'readTimestamp' => \DateTime::createFromFormat('U', (string) $time)->format(Timestamp::FORMAT) - ]); - - $this->connection->createSession(Argument::withEntry( - 'session', - ['labels' => [], 'creator_role' => 'Reader'] - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => self::SESSION - ]); - - $snapshot = $client->snapshot(); + $snapshot = $batchClient->snapshot(); } } diff --git a/Spanner/tests/Unit/Batch/BatchSnapshotTest.php b/Spanner/tests/Unit/Batch/BatchSnapshotTest.php index 84717d6f873f..3d61ade2a9d2 100644 --- a/Spanner/tests/Unit/Batch/BatchSnapshotTest.php +++ b/Spanner/tests/Unit/Batch/BatchSnapshotTest.php @@ -17,7 +17,7 @@ namespace Google\Cloud\Spanner\Tests\Unit\Batch; -use Google\Cloud\Core\Testing\TestHelpers; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Spanner\Batch\BatchSnapshot; use Google\Cloud\Spanner\Batch\PartitionInterface; use Google\Cloud\Spanner\Batch\QueryPartition; @@ -25,12 +25,17 @@ use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; use Google\Cloud\Spanner\Timestamp; -use Google\Cloud\Spanner\V1\SpannerClient; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\Partition; +use Google\Cloud\Spanner\V1\PartitionQueryRequest; +use Google\Cloud\Spanner\V1\PartitionReadRequest; +use Google\Cloud\Spanner\V1\PartitionResponse; +use Google\Cloud\Spanner\V1\ReadRequest; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -42,19 +47,20 @@ */ class BatchSnapshotTest extends TestCase { - use OperationRefreshTrait; use ProphecyTrait; use ResultGeneratorTrait; - use StubCreationTrait; + use ApiHelperTrait; const DATABASE = 'projects/my-awesome-project/instances/my-instance/databases/my-database'; const SESSION = 'projects/my-awesome-project/instances/my-instance/databases/my-database/sessions/session-id'; const TRANSACTION = 'transaction-id'; + private $spannerClient; + private $serializer; private $session; private $timestamp; - private $connection; private $snapshot; + private $stream; public function setUp(): void { @@ -68,29 +74,51 @@ public function setUp(): void $this->timestamp = new Timestamp(new \DateTime()); - $this->connection = $this->getConnStub(); - $this->snapshot = TestHelpers::stub(BatchSnapshot::class, [ - new Operation($this->connection->reveal(), false), + $this->serializer = new Serializer([], [], [], [ + 'google.spanner.v1.KeySet' => function ($keySet) { + $keys = $this->pluck('keys', $keySet, false); + if ($keys) { + $keySet['keys'] = array_map( + fn ($key) => $this->formatListForApi((array) $key), + $keys + ); + } + + if (isset($keySet['ranges'])) { + $keySet['ranges'] = array_map(function ($rangeItem) { + return array_map([$this, 'formatListForApi'], $rangeItem); + }, $keySet['ranges']); + + if (empty($keySet['ranges'])) { + unset($keySet['ranges']); + } + } + return $keySet; + }, + ]); + $this->spannerClient = $this->prophesize(SpannerClient::class); + + $this->snapshot = new BatchSnapshot( + new Operation($this->spannerClient->reveal(), $this->serializer), $this->session->reveal(), ['id' => self::TRANSACTION, 'readTimestamp' => $this->timestamp] - ], [ - 'operation', 'session' - ]); + ); } public function testClose() { $session = $this->prophesize(Session::class); - $session->delete([])->shouldBeCalled(); + $session->delete([])->shouldBeCalledOnce(); + + $this->snapshot = new BatchSnapshot( + $this->prophesize(Operation::class)->reveal(), + $session->reveal() + ); - $this->snapshot->___setProperty('session', $session->reveal()); $this->snapshot->close(); } - /** - * @dataProvider partitionReadAndQueryOptions - */ - public function testPartitionRead($testCaseOptions) + public function testPartitionRead() { $table = 'table'; $keySet = new KeySet(['all' => true]); @@ -99,15 +127,14 @@ public function testPartitionRead($testCaseOptions) 'index' => 'foo', 'maxPartitions' => 10, 'partitionSizeBytes' => 1 - ] + $testCaseOptions; + ]; $expectedArguments = [ 'session' => self::SESSION, - 'database' => self::DATABASE, - 'transactionId' => self::TRANSACTION, + 'transaction' => ['id' => self::TRANSACTION], 'table' => $table, 'columns' => $columns, - 'keySet' => $keySet->keySetObject(), + 'keySet' => $keySet->keySetObject() + ['keys' => [], 'ranges' => []], 'index' => $opts['index'], 'partitionOptions' => [ 'maxPartitions' => $opts['maxPartitions'], @@ -115,23 +142,18 @@ public function testPartitionRead($testCaseOptions) ] ]; - $expectedArguments += $testCaseOptions; - - $this->connection->partitionRead(Argument::that( - function ($actualArguments) use ($expectedArguments) { + $this->spannerClient->partitionRead( + Argument::that(function (PartitionReadRequest $request) use ($expectedArguments) { + $actualArguments = $this->serializer->encodeMessage($request); return $actualArguments == $expectedArguments; - } - ))->shouldBeCalled()->willReturn([ + }), + Argument::type('array') + )->shouldBeCalledOnce()->willReturn(new PartitionResponse([ 'partitions' => [ - [ - 'partitionToken' => 'token1' - ], [ - 'partitionToken' => 'token2' - ] + new Partition(['partition_token' => 'token1']), + new Partition(['partition_token' => 'token2']) ] - ]); - - $this->refreshOperation($this->snapshot, $this->connection->reveal()); + ])); $partitions = $this->snapshot->partitionRead($table, $keySet, $columns, $opts); $this->assertContainsOnlyInstancesOf(ReadPartition::class, $partitions); @@ -142,10 +164,7 @@ function ($actualArguments) use ($expectedArguments) { $this->assertEquals($opts, $partitions[0]->options()); } - /** - * @dataProvider partitionReadAndQueryOptions - */ - public function testPartitionQuery(array $testCaseOptions) + public function testPartitionQuery() { $sql = 'SELECT 1=1'; $opts = [ @@ -154,38 +173,32 @@ public function testPartitionQuery(array $testCaseOptions) ], 'maxPartitions' => 10, 'partitionSizeBytes' => 1 - ] + $testCaseOptions; + ]; $expectedArguments = [ 'session' => self::SESSION, - 'database' => self::DATABASE, - 'transactionId' => self::TRANSACTION, + 'transaction' => ['id' => self::TRANSACTION], 'sql' => $sql, 'params' => $opts['parameters'], - 'paramTypes' => ['foo' => ['code' => 6]], + 'paramTypes' => ['foo' => ['code' => 6, 'typeAnnotation' => 0, 'protoTypeFqn' => '']], 'partitionOptions' => [ 'maxPartitions' => $opts['maxPartitions'], 'partitionSizeBytes' => $opts['partitionSizeBytes'] ] ]; - $expectedArguments += $testCaseOptions; - - $this->connection->partitionQuery(Argument::that( - function ($actualArguments) use ($expectedArguments) { + $this->spannerClient->partitionQuery( + Argument::that(function (PartitionQueryRequest $request) use ($expectedArguments) { + $actualArguments = $this->serializer->encodeMessage($request); return $actualArguments == $expectedArguments; - } - ))->shouldBeCalled()->willReturn([ - 'partitions' => [ - [ - 'partitionToken' => 'token1' - ], [ - 'partitionToken' => 'token2' + }), + Argument::type('array') + )->shouldBeCalledOnce()->willReturn(new PartitionResponse([ + 'partitions' => [ + new Partition(['partition_token' => 'token1']), + new Partition(['partition_token' => 'token2']) ] - ] - ]); - - $this->refreshOperation($this->snapshot, $this->connection->reveal()); + ])); $partitions = $this->snapshot->partitionQuery($sql, $opts); $this->assertContainsOnlyInstancesOf(QueryPartition::class, $partitions); @@ -209,17 +222,25 @@ public function testExecuteQueryPartition() $partition = new QueryPartition($token, $sql, $opts); - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('partitionToken', $token), - Argument::withEntry('session', self::SESSION), - Argument::withEntry('database', self::DATABASE), - Argument::withEntry('transaction', ['id' => self::TRANSACTION]), - Argument::withEntry('sql', $sql), - Argument::withEntry('params', $opts['parameters']), - Argument::withEntry('paramTypes', ['foo' => ['code' => 6]]) - ))->shouldBeCalled()->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->snapshot, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql, $opts, $token) { + $this->assertEquals($request->getSql(), $sql); + $this->assertEquals($request->getSession(), self::SESSION); + $this->assertEquals($request->getTransaction()->getId(), self::TRANSACTION); + $this->assertEquals($request->getPartitionToken(), $token); + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['params'], $opts['parameters']); + $this->assertEquals( + $message['paramTypes'], + ['foo' => ['code' => 6, 'typeAnnotation' => 0, 'protoTypeFqn' => '']] + ); + return true; + }), + Argument::type('array') + )->shouldBeCalledOnce()->willReturn( + $this->resultGeneratorStream() + ); + $res = $this->snapshot->executePartition($partition); $this->assertInstanceOf(Result::class, $res); $rows = iterator_to_array($res->rows()); @@ -238,18 +259,25 @@ public function testExecuteReadPartition() $partition = new ReadPartition($token, $table, $keySet, $columns, $opts); - $this->connection->streamingRead(Argument::allOf( - Argument::withEntry('partitionToken', $token), - Argument::withEntry('session', self::SESSION), - Argument::withEntry('database', self::DATABASE), - Argument::withEntry('transaction', ['id' => self::TRANSACTION]), - Argument::withEntry('table', $table), - Argument::withEntry('columns', $columns), - Argument::withEntry('keySet', $keySet->keySetObject()), - Argument::withEntry('index', $opts['index']) - ))->shouldBeCalled()->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->snapshot, $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::that(function (ReadRequest $request) use ($token, $table, $columns, $keySet, $opts) { + $this->assertEquals($request->getSession(), self::SESSION); + $this->assertEquals($request->getPartitionToken(), $token); + $this->assertEquals($request->getTable(), $table); + $this->assertEquals($request->getIndex(), $opts['index']); + $this->assertEquals(iterator_to_array($request->getColumns()), $columns); + $this->assertEquals( + $request->getTransaction()->getId(), + self::TRANSACTION + ); + $this->assertTrue($this->serializer->encodeMessage($request->getKeySet())['all']); + return true; + }), + Argument::type('array') + )->shouldBeCalledOnce()->willReturn( + $this->resultGeneratorStream() + ); + $res = $this->snapshot->executePartition($partition); $this->assertInstanceOf(Result::class, $res); $rows = iterator_to_array($res->rows()); @@ -273,24 +301,7 @@ public function testExecutePartitionInvalidType() $this->expectException(\BadMethodCallException::class); $this->expectExceptionMessage('Unsupported partition type.'); - $dummy = new DummyPartition; - $this->snapshot->executePartition($dummy); + $dummy = $this->prophesize(PartitionInterface::class); + $this->snapshot->executePartition($dummy->reveal()); } - - public function partitionReadAndQueryOptions() - { - return [ - [['dataBoostEnabled' => false]], - [['dataBoostEnabled' => true]] - ]; - } -} - -//@codingStandardsIgnoreStart -class DummyPartition implements PartitionInterface -{ - public function __toString() {} - public function serialize() {} - public static function hydrate(array $data) {} } -//@codingStandardsIgnoreEnd diff --git a/Spanner/tests/Unit/BytesTest.php b/Spanner/tests/Unit/BytesTest.php index e923a72c702e..104276ee2685 100644 --- a/Spanner/tests/Unit/BytesTest.php +++ b/Spanner/tests/Unit/BytesTest.php @@ -17,8 +17,8 @@ namespace Google\Cloud\Spanner\Tests\Unit; -use Google\Cloud\Spanner\Bytes; use Google\Cloud\Core\Testing\GrpcTestTrait; +use Google\Cloud\Spanner\Bytes; use PHPUnit\Framework\TestCase; /** diff --git a/Spanner/tests/Unit/CommitTimestampTest.php b/Spanner/tests/Unit/CommitTimestampTest.php index 782de7af676f..9dd2fc9ef6a6 100644 --- a/Spanner/tests/Unit/CommitTimestampTest.php +++ b/Spanner/tests/Unit/CommitTimestampTest.php @@ -30,7 +30,7 @@ class CommitTimestampTest extends TestCase public function setUp(): void { - $this->t = new CommitTimestamp; + $this->t = new CommitTimestamp(); } public function testType() diff --git a/Spanner/tests/Unit/Connection/GrpcTest.php b/Spanner/tests/Unit/Connection/GrpcTest.php deleted file mode 100644 index 01f711657ae7..000000000000 --- a/Spanner/tests/Unit/Connection/GrpcTest.php +++ /dev/null @@ -1,1722 +0,0 @@ -<?php -/** - * Copyright 2016 Google Inc. - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit\Connection; - -use Google\ApiCore\Call; -use Google\ApiCore\CredentialsWrapper; -use Google\ApiCore\OperationResponse; -use Google\ApiCore\Serializer; -use Google\ApiCore\Testing\MockResponse; -use Google\ApiCore\Transport\TransportInterface; -use Google\Cloud\Core\GrpcRequestWrapper; -use Google\Cloud\Core\GrpcTrait; -use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Spanner\Admin\Database\V1\Backup; -use Google\Cloud\Spanner\Admin\Database\V1\CreateBackupEncryptionConfig; -use Google\Cloud\Spanner\Admin\Database\V1\EncryptionConfig; -use Google\Cloud\Spanner\Admin\Database\V1\RestoreDatabaseEncryptionConfig; -use Google\Cloud\Spanner\Admin\Instance\V1\Instance; -use Google\Cloud\Spanner\Admin\Instance\V1\Instance\State; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; -use Google\Cloud\Spanner\Connection\Grpc; -use Google\Cloud\Spanner\V1\BatchWriteRequest\MutationGroup as MutationGroupProto; -use Google\Cloud\Spanner\V1\DeleteSessionRequest; -use Google\Cloud\Spanner\V1\ExecuteBatchDmlRequest\Statement; -use Google\Cloud\Spanner\V1\ExecuteSqlRequest\QueryOptions; -use Google\Cloud\Spanner\V1\KeySet; -use Google\Cloud\Spanner\V1\Mutation; -use Google\Cloud\Spanner\V1\Mutation\Delete; -use Google\Cloud\Spanner\V1\Mutation\Write; -use Google\Cloud\Spanner\V1\PartialResultSet; -use Google\Cloud\Spanner\V1\PartitionOptions; -use Google\Cloud\Spanner\V1\RequestOptions; -use Google\Cloud\Spanner\V1\Session; -use Google\Cloud\Spanner\V1\SpannerClient; -use Google\Cloud\Spanner\V1\TransactionOptions; -use Google\Cloud\Spanner\V1\TransactionOptions\PartitionedDml; -use Google\Cloud\Spanner\V1\TransactionOptions\PBReadOnly; -use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite; -use Google\Cloud\Spanner\V1\TransactionSelector; -use Google\Cloud\Spanner\V1\Type; -use Google\Cloud\Spanner\ValueMapper; -use Google\Cloud\Spanner\MutationGroup; -use Google\Protobuf\FieldMask; -use Google\Protobuf\ListValue; -use Google\Protobuf\NullValue; -use Google\Protobuf\Struct; -use Google\Protobuf\Timestamp; -use Google\Protobuf\Value; -use GuzzleHttp\Promise\PromiseInterface; -use http\Exception\InvalidArgumentException; -use PHPUnit\Framework\TestCase; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; - -/** - * @group spanner - * @group spanner-grpc - */ -class GrpcTest extends TestCase -{ - use GrpcTestTrait; - use GrpcTrait; - use ProphecyTrait; - - const CONFIG = 'projects/my-project/instanceConfigs/config-1'; - const DATABASE = 'projects/my-project/instances/instance-1/databases/database-1'; - const INSTANCE = 'projects/my-project/instances/instance-1'; - const PROJECT = 'projects/my-project'; - const SESSION = 'projects/my-project/instances/instance-1/databases/database-1/sessions/session-1'; - const TABLE = 'table-1'; - const TRANSACTION = 'transaction-1'; - - private $requestWrapper; - private $serializer; - private $successMessage; - private $lro; - - public function setUp(): void - { - $this->checkAndSkipGrpcTests(); - - $this->requestWrapper = $this->prophesize(GrpcRequestWrapper::class); - $this->serializer = new Serializer; - $this->successMessage = 'success'; - $this->lro = $this->prophesize(OperationResponse::class)->reveal(); - } - - public function testApiEndpoint() - { - $expected = 'foobar.com'; - - $grpc = new GrpcStub(['apiEndpoint' => $expected]); - - $this->assertEquals($expected, $grpc->config['apiEndpoint']); - } - - /** - * @dataProvider clientUniverseDomainConfigProvider - */ - public function testUniverseDomain($config, $expectedUniverseDomain, ?string $envUniverse = null) - { - if ($envUniverse) { - putenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN=' . $envUniverse); - } - - $grpc = new GrpcStub($config); - - if ($envUniverse) { - // We have to do this instead of using "@runInSeparateProcess" because in the case of - // an error, PHPUnit throws a "Serialization of 'ReflectionClass' is not allowed" error. - // @TODO: Remove this once we've updated to PHPUnit 10. - putenv('GOOGLE_CLOUD_UNIVERSE_DOMAIN'); - } - - $this->assertEquals($expectedUniverseDomain, $grpc->config['universeDomain']); - } - - public function clientUniverseDomainConfigProvider() - { - return [ - [[], 'googleapis.com'], - [['universeDomain' => 'googleapis.com'], 'googleapis.com'], - [['universeDomain' => 'abc.def.ghi'], 'abc.def.ghi'], - [[], 'abc.def.ghi', 'abc.def.ghi'], - [['universeDomain' => 'googleapis.com'], 'googleapis.com', 'abc.def.ghi'], - ]; - } - - public function testListInstanceConfigs() - { - $this->assertCallCorrect('listInstanceConfigs', [ - 'projectName' => self::PROJECT - ], $this->expectResourceHeader(self::PROJECT, [ - self::PROJECT - ])); - } - - public function testGetInstanceConfig() - { - $this->assertCallCorrect('getInstanceConfig', [ - 'name' => self::CONFIG, - 'projectName' => self::PROJECT - ], $this->expectResourceHeader(self::PROJECT, [ - self::CONFIG - ])); - } - - public function testCreateInstanceConfig() - { - list ($args, $config) = $this->instanceConfig(); - - $this->assertCallCorrect( - 'createInstanceConfig', - [ - 'projectName' => self::PROJECT, - 'instanceConfigId' => self::CONFIG - ] + $args, - $this->expectResourceHeader(self::CONFIG, [ - self::PROJECT, - self::CONFIG, - $config - ]), - $this->lro, - null - ); - } - - public function testUpdateInstanceConfig() - { - list ($args, $config, $fieldMask) = $this->instanceConfig(false); - $this->assertCallCorrect('updateInstanceConfig', $args, $this->expectResourceHeader(self::CONFIG, [ - $config, $fieldMask - ]), $this->lro, null); - } - - public function testDeleteInstanceConfig() - { - $this->assertCallCorrect('deleteInstanceConfig', [ - 'name' => self::CONFIG - ], $this->expectResourceHeader(self::CONFIG, [ - self::CONFIG - ])); - } - - public function testListInstances() - { - $this->assertCallCorrect('listInstances', [ - 'projectName' => self::PROJECT - ], $this->expectResourceHeader(self::PROJECT, [ - self::PROJECT - ])); - } - - public function testGetInstance() - { - $this->assertCallCorrect('getInstance', [ - 'name' => self::INSTANCE, - 'projectName' => self::PROJECT - ], $this->expectResourceHeader(self::PROJECT, [ - self::INSTANCE - ])); - } - - public function testGetInstanceWithFieldMaskArray() - { - $fieldNames = ['name', 'displayName', 'nodeCount']; - - $mask = []; - foreach (array_values($fieldNames) as $key) { - $mask[] = Serializer::toSnakeCase($key); - } - - $fieldMask = $this->serializer->decodeMessage(new FieldMask, ['paths' => $mask]); - $this->assertCallCorrect('getInstance', [ - 'name' => self::INSTANCE, - 'projectName' => self::PROJECT, - 'fieldMask' => $fieldNames - ], $this->expectResourceHeader(self::PROJECT, [ - self::INSTANCE, - ['fieldMask' => $fieldMask] - ])); - } - - public function testGetInstanceWithFieldMaskString() - { - $fieldNames = 'nodeCount'; - $mask[] = Serializer::toSnakeCase($fieldNames); - - $fieldMask = $this->serializer->decodeMessage(new FieldMask, ['paths' => $mask]); - $this->assertCallCorrect('getInstance', [ - 'name' => self::INSTANCE, - 'projectName' => self::PROJECT, - 'fieldMask' => $fieldNames - ], $this->expectResourceHeader(self::PROJECT, [ - self::INSTANCE, - ['fieldMask' => $fieldMask] - ])); - } - - public function testCreateInstance() - { - list ($args, $instance) = $this->instance(); - - $this->assertCallCorrect('createInstance', [ - 'projectName' => self::PROJECT, - 'instanceId' => self::INSTANCE - ] + $args, $this->expectResourceHeader(self::INSTANCE, [ - self::PROJECT, - self::INSTANCE, - $instance - ]), $this->lro, null); - } - - public function testCreateInstanceWithProcessingNodes() - { - list ($args, $instance) = $this->instance(true, false); - - $this->assertCallCorrect('createInstance', [ - 'projectName' => self::PROJECT, - 'instanceId' => self::INSTANCE, - 'processingUnits' => 1000 - ] + $args, $this->expectResourceHeader(self::INSTANCE, [ - self::PROJECT, - self::INSTANCE, - $instance - ]), $this->lro, null); - } - - public function testUpdateInstance() - { - list ($args, $instance, $fieldMask) = $this->instance(false); - - $this->assertCallCorrect('updateInstance', $args, $this->expectResourceHeader(self::INSTANCE, [ - $instance, $fieldMask - ]), $this->lro, null); - } - - public function testDeleteInstance() - { - $this->assertCallCorrect('deleteInstance', [ - 'name' => self::INSTANCE - ], $this->expectResourceHeader(self::INSTANCE, [ - self::INSTANCE - ])); - } - - public function testSetInstanceIamPolicy() - { - $policy = ['foo' => 'bar']; - - $this->assertCallCorrect('setInstanceIamPolicy', [ - 'resource' => self::INSTANCE, - 'policy' => $policy - ], $this->expectResourceHeader(self::INSTANCE, [ - self::INSTANCE, - $policy - ], false)); - } - - public function testGetInstanceIamPolicy() - { - $this->assertCallCorrect('getInstanceIamPolicy', [ - 'resource' => self::INSTANCE - ], $this->expectResourceHeader(self::INSTANCE, [ - self::INSTANCE - ])); - } - - public function testTestInstanceIamPermissions() - { - $permissions = ['permission1', 'permission2']; - $this->assertCallCorrect('testInstanceIamPermissions', [ - 'resource' => self::INSTANCE, - 'permissions' => $permissions - ], $this->expectResourceHeader(self::INSTANCE, [ - self::INSTANCE, - $permissions - ], false)); - } - - public function testListDatabases() - { - $this->assertCallCorrect('listDatabases', [ - 'instance' => self::INSTANCE - ], $this->expectResourceHeader(self::INSTANCE, [ - self::INSTANCE - ])); - } - - public function testCreateDatabase() - { - $createStmt = 'CREATE Foo'; - $extraStmts = [ - 'CREATE TABLE Bar' - ]; - $encryptionConfig = ['kmsKeyName' => 'kmsKeyName']; - $expectedEncryptionConfig = $this->serializer->decodeMessage(new EncryptionConfig, $encryptionConfig); - - $this->assertCallCorrect('createDatabase', [ - 'instance' => self::INSTANCE, - 'createStatement' => $createStmt, - 'extraStatements' => $extraStmts, - 'encryptionConfig' => $encryptionConfig - ], $this->expectResourceHeader(self::INSTANCE, [ - self::INSTANCE, - $createStmt, - [ - 'extraStatements' => $extraStmts, - 'encryptionConfig' => $expectedEncryptionConfig - ] - ]), $this->lro, null); - } - - public function testCreateBackup() - { - $backupId = "backup-id"; - $expireTime = new \DateTime("+ 7 hours"); - $backup = [ - 'database' => self::DATABASE, - 'expireTime' => $expireTime->format('Y-m-d\TH:i:s.u\Z') - ]; - $expectedBackup = $this->serializer->decodeMessage(new Backup(), [ - 'expireTime' => $this->formatTimestampForApi($backup['expireTime']) - ] + $backup); - - $encryptionConfig = [ - 'kmsKeyName' => 'kmsKeyName', - 'encryptionType' => CreateBackupEncryptionConfig\EncryptionType::CUSTOMER_MANAGED_ENCRYPTION - ]; - $expectedEncryptionConfig = $this->serializer->decodeMessage( - new CreateBackupEncryptionConfig, - $encryptionConfig - ); - - $this->assertCallCorrect('createBackup', [ - 'instance' => self::INSTANCE, - 'backupId' => $backupId, - 'backup' => $backup, - 'encryptionConfig' => $encryptionConfig - ], $this->expectResourceHeader(self::INSTANCE, [ - self::INSTANCE, - $backupId, - $expectedBackup, - [ - 'encryptionConfig' => $expectedEncryptionConfig - ] - ]), $this->lro, null); - } - - public function testRestoreDatabase() - { - $databaseId = 'test-database'; - $encryptionConfig = [ - 'kmsKeyName' => 'kmsKeyName', - 'encryptionType' => RestoreDatabaseEncryptionConfig\EncryptionType::CUSTOMER_MANAGED_ENCRYPTION - ]; - $expectedEncryptionConfig = $this->serializer->decodeMessage( - new RestoreDatabaseEncryptionConfig, - $encryptionConfig - ); - - $this->assertCallCorrect('restoreDatabase', [ - 'instance' => self::INSTANCE, - 'databaseId' => $databaseId, - 'encryptionConfig' => $encryptionConfig - ], $this->expectResourceHeader(self::INSTANCE, [ - self::INSTANCE, - $databaseId, - [ - 'encryptionConfig' => $expectedEncryptionConfig - ] - ]), $this->lro, null); - } - - public function testUpdateDatabaseDdl() - { - $statements = [ - 'CREATE TABLE Bar' - ]; - - $this->assertCallCorrect('updateDatabaseDdl', [ - 'name' => self::DATABASE, - 'statements' => $statements - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE, - $statements - ], false), $this->lro, null); - } - - public function testDropDatabase() - { - $this->assertCallCorrect('dropDatabase', [ - 'name' => self::DATABASE - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE - ])); - } - - public function testGetDatabase() - { - $this->assertCallCorrect('getDatabase', [ - 'name' => self::DATABASE - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE - ])); - } - - public function testGetDatabaseDdl() - { - $this->assertCallCorrect('getDatabaseDdl', [ - 'name' => self::DATABASE - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE - ])); - } - - public function testSetDatabaseIamPolicy() - { - $policy = ['foo' => 'bar']; - - $this->assertCallCorrect('setDatabaseIamPolicy', [ - 'resource' => self::DATABASE, - 'policy' => $policy - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE, - $policy - ], false)); - } - - public function testGetDatabaseIamPolicy() - { - $this->assertCallCorrect('getDatabaseIamPolicy', [ - 'resource' => self::DATABASE - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE - ])); - } - - public function testTestDatabaseIamPermissions() - { - $permissions = ['permission1', 'permission2']; - $this->assertCallCorrect('testDatabaseIamPermissions', [ - 'resource' => self::DATABASE, - 'permissions' => $permissions - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE, - $permissions - ], false)); - } - - /** - * @dataProvider larOptions - */ - public function testCreateSession($larEnabled, $grpcConfig) - { - $labels = ['foo' => 'bar']; - - $this->assertCallCorrect('createSession', [ - 'database' => self::DATABASE, - 'session' => [ - 'labels' => $labels - ] - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE, - [ - 'session' => (new Session)->setLabels($labels) - ] - ], true, $larEnabled), null, '', $grpcConfig); - } - - public function testCreateSessionAsync() - { - $promise = $this->prophesize(PromiseInterface::class)->reveal(); - $client = $this->prophesize(SpannerClient::class); - $transport = $this->prophesize(TransportInterface::class); - $transport->startUnaryCall( - Argument::type(Call::class), - Argument::withEntry('headers', [ - 'x-goog-spanner-route-to-leader' => ['true'], - 'google-cloud-resource-prefix' => ['database1'] - ]) - )->willReturn($promise); - - $client->getTransport()->willReturn($transport->reveal()); - - $grpc = new Grpc(['gapicSpannerClient' => $client->reveal()]); - - $promise = $grpc->createSessionAsync([ - 'database' => 'database1', - 'session' => [ - 'labels' => [ 'foo' => 'bar' ] - ] - ]); - - $this->assertInstanceOf(PromiseInterface::class, $promise); - } - - /** - * @dataProvider larOptions - */ - public function testBatchCreateSessions($larEnabled, $grpcConfig) - { - $count = 10; - $template = [ - 'labels' => [ - 'foo' => 'bar' - ] - ]; - - $this->assertCallCorrect('batchCreateSessions', [ - 'database' => self::DATABASE, - 'sessionCount' => $count, - 'sessionTemplate' => $template - ], $this->expectResourceHeader(self::DATABASE, [ - self::DATABASE, $count, [ - 'sessionTemplate' => $this->serializer->decodeMessage(new Session, $template) - ] - ], true, $larEnabled), null, '', $grpcConfig); - } - - public function testBatchWrite() - { - $mutationGroups = [ - (new MutationGroup(false)) - ->insertOrUpdate( - "Singers", - ['SingerId' => 16, 'FirstName' => 'Scarlet', 'LastName' => 'Terry'] - )->toArray(), - (new MutationGroup(false)) - ->insertOrUpdate( - "Singers", - ['SingerId' => 17, 'FirstName' => 'Marc', 'LastName' => 'Kristen'] - )->insertOrUpdate( - "Albums", - ['AlbumId' => 1, 'SingerId' => 17, 'AlbumTitle' => 'Total Junk'] - )->toArray() - ]; - - $expectedMutationGroups = [ - new MutationGroupProto(['mutations' => [ - new Mutation(['insert_or_update' => new Write([ - 'table' => 'Singers', - 'columns' => ['SingerId', 'FirstName', 'LastName'], - 'values' => [new ListValue(['values' => [ - new Value(['string_value' => '16']), - new Value(['string_value' => 'Scarlet']), - new Value(['string_value' => 'Terry']) - ]])] - ])]) - ]]), - new MutationGroupProto(['mutations' => [ - new Mutation(['insert_or_update' => new Write([ - 'table' => 'Singers', - 'columns' => ['SingerId', 'FirstName', 'LastName'], - 'values' => [new ListValue(['values' => [ - new Value(['string_value' => '17']), - new Value(['string_value' => 'Marc']), - new Value(['string_value' => 'Kristen']) - ]])] - ])]), - new Mutation(['insert_or_update' => new Write([ - 'table' => 'Albums', - 'columns' => ['AlbumId', 'SingerId', 'AlbumTitle'], - 'values' => [new ListValue(['values' => [ - new Value(['string_value' => '1']), - new Value(['string_value' => '17']), - new Value(['string_value' => 'Total Junk']) - ]])] - ])]), - ]]), - ]; - - $this->assertCallCorrect( - 'batchWrite', - [ - 'database' => self::DATABASE, - 'session' => self::SESSION, - 'mutationGroups' => $mutationGroups, - ], - $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $expectedMutationGroups, - [] - ]), - ); - } - - /** - * @dataProvider larOptions - */ - public function testGetSession($larEnabled, $grpcConfig) - { - $this->assertCallCorrect('getSession', [ - 'database' => self::DATABASE, - 'name' => self::SESSION - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION - ], true, $larEnabled), null, '', $grpcConfig); - } - - public function testDeleteSession() - { - $this->assertCallCorrect('deleteSession', [ - 'database' => self::DATABASE, - 'name' => self::SESSION - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION - ])); - } - - public function testDeleteSessionAsync() - { - $promise = $this->prophesize(PromiseInterface::class) - ->reveal(); - $sessionName = 'session1'; - $databaseName = 'database1'; - $request = new DeleteSessionRequest(); - $request->setName($sessionName); - $client = $this->prophesize(SpannerClient::class); - $transport = $this->prophesize(TransportInterface::class); - $transport->startUnaryCall( - Argument::type(Call::class), - Argument::type('array') - )->willReturn($promise); - $client->getTransport() - ->willReturn($transport->reveal()); - $grpc = new Grpc(['gapicSpannerClient' => $client->reveal()]); - $call = $grpc->deleteSessionAsync([ - 'name' => $sessionName, - 'database' => $databaseName - ]); - - $this->assertInstanceOf(PromiseInterface::class, $call); - } - - /** - * @dataProvider larOptions - */ - public function testExecuteStreamingSql($larEnabled, $grpcConfig) - { - $sql = 'SELECT 1'; - - $mapper = new ValueMapper(false); - $mapped = $mapper->formatParamsForExecuteSql(['foo' => 'bar']); - - $expectedParams = $this->serializer->decodeMessage( - new Struct, - $this->formatStructForApi($mapped['params']) - ); - - $expectedParamTypes = $mapped['paramTypes']; - foreach ($expectedParamTypes as $key => $param) { - $expectedParamTypes[$key] = $this->serializer->decodeMessage(new Type, $param); - } - - $this->assertCallCorrect('executeStreamingSql', [ - 'session' => self::SESSION, - 'sql' => $sql, - 'transactionId' => self::TRANSACTION, - 'database' => self::DATABASE, - 'headers' => ['x-goog-spanner-route-to-leader' => ['true']] - ] + $mapped, $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $sql, - [ - 'transaction' => $this->transactionSelector(), - 'params' => $expectedParams, - 'paramTypes' => $expectedParamTypes - ] - ], true, $larEnabled), null, '', $grpcConfig); - } - - public function testExecuteStreamingSqlWithRequestOptions() - { - $sql = 'SELECT 1'; - $requestOptions = ["priority" => RequestOptions\Priority::PRIORITY_LOW]; - $expectedRequestOptions = $this->serializer->decodeMessage( - new RequestOptions, - $requestOptions - ); - - $this->assertCallCorrect('executeStreamingSql', [ - 'session' => self::SESSION, - 'sql' => $sql, - 'transactionId' => self::TRANSACTION, - 'database' => self::DATABASE, - 'params' => [], - 'requestOptions' => $requestOptions - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $sql, - [ - 'transaction' => $this->transactionSelector(), - 'requestOptions' => $expectedRequestOptions - ] - ])); - } - - /** - * @dataProvider queryOptions - */ - public function testExecuteStreamingSqlWithQueryOptions( - array $methodOptions, - array $envOptions, - array $clientOptions, - array $expectedOptions - ) { - $sql = 'SELECT 1'; - - if (array_key_exists('optimizerVersion', $envOptions)) { - putenv('SPANNER_OPTIMIZER_VERSION=' . $envOptions['optimizerVersion']); - } - if (array_key_exists('optimizerStatisticsPackage', $envOptions)) { - putenv('SPANNER_OPTIMIZER_STATISTICS_PACKAGE=' . $envOptions['optimizerStatisticsPackage']); - } - $gapic = $this->prophesize(SpannerClient::class); - $gapic->executeStreamingSql( - self::SESSION, - $sql, - Argument::that(function ($arguments) use ($expectedOptions) { - $queryOptions = $arguments['queryOptions'] ?? null; - $expectedOptions += ['optimizerVersion' => null, 'optimizerStatisticsPackage' => null]; - $this->assertEquals( - $queryOptions ? $queryOptions->getOptimizerVersion() : null, - $expectedOptions['optimizerVersion'] - ); - $this->assertEquals( - $queryOptions ? $queryOptions->getOptimizerStatisticsPackage() : null, - $expectedOptions['optimizerStatisticsPackage'] - ); - return true; - }) - )->shouldBeCalledOnce(); - - $grpc = new Grpc([ - 'gapicSpannerClient' => $gapic->reveal() - ] + ['queryOptions' => $clientOptions]); - - $grpc->executeStreamingSql([ - 'database' => self::DATABASE, - 'session' => self::SESSION, - 'sql' => $sql, - 'params' => [] - ] + ['queryOptions' => $methodOptions]); - - if ($envOptions) { - putenv('SPANNER_OPTIMIZER_VERSION='); - putenv('SPANNER_OPTIMIZER_STATISTICS_PACKAGE='); - } - } - - public function queryOptions() - { - return [ - [ - ['optimizerVersion' => '8'], - [ - 'optimizerVersion' => '7', - 'optimizerStatisticsPackage' => "auto_20191128_18_47_22UTC", - ], - ['optimizerStatisticsPackage' => "auto_20191128_14_47_22UTC"], - [ - 'optimizerVersion' => '8', - 'optimizerStatisticsPackage' => "auto_20191128_18_47_22UTC", - ] - ], - [ - [], - ['optimizerVersion' => '7'], - [ - 'optimizerVersion' => '6', - 'optimizerStatisticsPackage' => "auto_20191128_14_47_22UTC", - ], - [ - 'optimizerVersion' => '7', - 'optimizerStatisticsPackage' => "auto_20191128_14_47_22UTC", - ] - ], - [ - ['optimizerStatisticsPackage' => "auto_20191128_23_47_22UTC"], - [], - [ - 'optimizerVersion' => '6', - 'optimizerStatisticsPackage' => "auto_20191128_14_47_22UTC", - ], - [ - 'optimizerVersion' => '6', - 'optimizerStatisticsPackage' => "auto_20191128_23_47_22UTC", - ] - ], - [ - [], - [], - [], - [] - ] - ]; - } - - /** - * @dataProvider readKeysets - */ - public function testStreamingRead($keyArg, $keyObj, $larEnabled, $grpcConfig) - { - $columns = [ - 'id', - 'name' - ]; - - $this->assertCallCorrect('streamingRead', [ - 'keySet' => $keyArg, - 'transactionId' => self::TRANSACTION, - 'session' => self::SESSION, - 'table' => self::TABLE, - 'columns' => $columns, - 'database' => self::DATABASE, - 'headers' => ['x-goog-spanner-route-to-leader' => ['true']] - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - self::TABLE, - $columns, - $keyObj, - [ - 'transaction' => $this->transactionSelector() - ] - ], true, $larEnabled), null, '', $grpcConfig); - } - - public function testStreamingReadWithRequestOptions() - { - $columns = [ - 'id', - 'name' - ]; - $requestOptions = ['priority' => RequestOptions\Priority::PRIORITY_LOW]; - $expectedRequestOptions = $this->serializer->decodeMessage( - new RequestOptions, - $requestOptions - ); - - $this->assertCallCorrect('streamingRead', [ - 'keySet' => [], - 'transactionId' => self::TRANSACTION, - 'session' => self::SESSION, - 'table' => self::TABLE, - 'columns' => $columns, - 'database' => self::DATABASE, - 'requestOptions' => $requestOptions - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - self::TABLE, - $columns, - new KeySet, - [ - 'transaction' => $this->transactionSelector(), - 'requestOptions' => $expectedRequestOptions - ] - ])); - } - - public function readKeysets() - { - $this->setUp(); - - return [ - [ - [], - new KeySet, - true, - ['routeToLeader' => true] - ], [ - ['keys' => [1]], - $this->serializer->decodeMessage(new KeySet, [ - 'keys' => [ - [ - 'values' => [ - [ - 'number_value' => 1 - ] - ] - ] - ] - ]), - false, - ['routeToLeader' => false] - ], [ - ['keys' => [[1,1]]], - $this->serializer->decodeMessage(new KeySet, [ - 'keys' => [ - [ - 'values' => [ - [ - 'number_value' => 1 - ], - [ - 'number_value' => 1 - ] - ] - ] - ] - ]), - false, - ['routeToLeader' => false] - ] - ]; - } - - /** - * @dataProvider larOptions - */ - public function testExecuteBatchDml($larEnabled, $grpcConfig) - { - $statements = [ - [ - 'sql' => 'SELECT 1', - 'params' => [] - ] - ]; - - $statementsObjs = [ - new Statement([ - 'sql' => 'SELECT 1' - ]) - ]; - - $this->assertCallCorrect('executeBatchDml', [ - 'session' => self::SESSION, - 'database' => self::DATABASE, - 'transactionId' => self::TRANSACTION, - 'statements' => $statements, - 'seqno' => 1 - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $this->transactionSelector(), - $statementsObjs, - 1 - ], true, $larEnabled), null, '', $grpcConfig); - } - - public function testExecuteBatchDmlWithRequestOptions() - { - $statements = [ - [ - 'sql' => 'SELECT 1', - 'params' => [] - ] - ]; - - $statementsObjs = [ - new Statement([ - 'sql' => 'SELECT 1' - ]) - ]; - $requestOptions = ['priority' => RequestOptions\Priority::PRIORITY_LOW]; - $expectedRequestOptions = $this->serializer->decodeMessage( - new RequestOptions, - $requestOptions - ); - - - $this->assertCallCorrect('executeBatchDml', [ - 'session' => self::SESSION, - 'database' => self::DATABASE, - 'transactionId' => self::TRANSACTION, - 'statements' => $statements, - 'seqno' => 1, - 'requestOptions' => $requestOptions - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $this->transactionSelector(), - $statementsObjs, - 1, - ['requestOptions' => $expectedRequestOptions] - ], true, true)); - } - - /** - * @dataProvider transactionTypes - */ - public function testBeginTransaction($optionsArr, $optionsObj, $larEnabled, $grpcConfig) - { - $this->assertCallCorrect('beginTransaction', [ - 'session' => self::SESSION, - 'transactionOptions' => $optionsArr, - 'database' => self::DATABASE - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $optionsObj - ], true, $larEnabled, $optionsArr), null, '', $grpcConfig); - } - - public function transactionTypes() - { - $ts = (new \DateTime)->format('Y-m-d\TH:i:s.u\Z'); - $pbTs = new Timestamp($this->formatTimestampForApi($ts)); - $readOnlyClass = PHP_VERSION_ID >= 80100 - ? PBReadOnly::class - : 'Google\Cloud\Spanner\V1\TransactionOptions\ReadOnly'; - - return [ - [ - ['readWrite' => []], - new TransactionOptions([ - 'read_write' => new ReadWrite - ]), - true, - ['routeToLeader' => true] - ], [ - [ - 'readOnly' => [ - 'minReadTimestamp' => $ts, - 'readTimestamp' => $ts - ] - ], - new TransactionOptions([ - 'read_only' => new $readOnlyClass([ - 'min_read_timestamp' => $pbTs, - 'read_timestamp' => $pbTs - ]) - ]), - true, - ['routeToLeader' => true] - ], [ - ['partitionedDml' => []], - new TransactionOptions([ - 'partitioned_dml' => new PartitionedDml - ]), - true, - ['routeToLeader' => true] - ] - ]; - } - - /** - * @dataProvider commit - */ - public function testCommit($mutationsArr, $mutationsObjArr, $larEnabled, $grpcConfig) - { - $this->assertCallCorrect('commit', [ - 'session' => self::SESSION, - 'mutations' => $mutationsArr, - 'singleUseTransaction' => true, - 'database' => self::DATABASE - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $mutationsObjArr, - [ - 'singleUseTransaction' => new TransactionOptions([ - 'read_write' => new ReadWrite - ]) - ] - ], true, $grpcConfig), null, '', $grpcConfig); - } - - /** - * @dataProvider commit - */ - public function testCommitWithRequestOptions($mutationsArr, $mutationsObjArr) - { - $requestOptions = ['priority' => RequestOptions\Priority::PRIORITY_LOW]; - $expectedRequestOptions = $this->serializer->decodeMessage( - new RequestOptions, - $requestOptions - ); - $this->assertCallCorrect('commit', [ - 'session' => self::SESSION, - 'mutations' => $mutationsArr, - 'singleUseTransaction' => true, - 'database' => self::DATABASE, - 'requestOptions' => $requestOptions - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $mutationsObjArr, - [ - 'singleUseTransaction' => new TransactionOptions([ - 'read_write' => new ReadWrite - ]), - 'requestOptions' => $expectedRequestOptions - ] - ], true, true)); - } - - public function commit() - { - $mutation = [ - 'table' => self::TABLE, - 'columns' => [ - 'col1' - ], - 'values' => [ - 'val1' - ] - ]; - - $write = new Write([ - 'table' => self::TABLE, - 'columns' => ['col1'], - 'values' => [ - new ListValue([ - 'values' => [ - new Value([ - 'string_value' => 'val1' - ]) - ] - ]) - ] - ]); - - return [ - [ - [], [], true, ['routeToLeader' => true] - ], [ - [ - [ - 'delete' => [ - 'table' => self::TABLE, - 'keySet' => [] - ] - ] - ], - [ - new Mutation([ - 'delete' => new Delete([ - 'table' => self::TABLE, - 'key_set' => new KeySet - ]) - ]) - ], - true, - ['routeToLeader' => true] - ], [ - [ - [ - 'insert' => $mutation - ] - ], - [ - new Mutation([ - 'insert' => $write - ]) - ], - true, - ['routeToLeader' => true] - ], [ - [ - [ - 'update' => $mutation - ] - ], - [ - new Mutation([ - 'update' => $write - ]) - ], - true, - ['routeToLeader' => true] - ], [ - [ - [ - 'insertOrUpdate' => $mutation - ] - ], - [ - new Mutation([ - 'insert_or_update' => $write - ]) - ], - true, - ['routeToLeader' => true] - ], [ - [ - [ - 'replace' => $mutation - ] - ], - [ - new Mutation([ - 'replace' => $write - ]) - ], - true, - ['routeToLeader' => true] - ] - ]; - } - - /** - * @dataProvider larOptions - */ - public function testRollback($larEnabled, $grpcConfig) - { - $this->assertCallCorrect('rollback', [ - 'session' => self::SESSION, - 'transactionId' => self::TRANSACTION, - 'database' => self::DATABASE - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - self::TRANSACTION - ], true, $larEnabled), null, '', $grpcConfig); - } - - /** - * @dataProvider partitionOptions - */ - public function testPartitionQuery($partitionOptions, $partitionOptionsObj, $larEnabled, $grpcConfig) - { - $sql = 'SELECT 1'; - $this->assertCallCorrect('partitionQuery', [ - 'session' => self::SESSION, - 'sql' => $sql, - 'params' => [], - 'transactionId' => self::TRANSACTION, - 'database' => self::DATABASE, - 'partitionOptions' => $partitionOptions, - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - $sql, - [ - 'transaction' => $this->transactionSelector(), - 'partitionOptions' => $partitionOptionsObj - ] - ], true, $larEnabled), null, '', $grpcConfig); - } - - /** - * @dataProvider partitionOptions - */ - public function testPartitionRead($partitionOptions, $partitionOptionsObj, $larEnabled, $grpcConfig) - { - $this->assertCallCorrect('partitionRead', [ - 'session' => self::SESSION, - 'keySet' => [], - 'table' => self::TABLE, - 'transactionId' => self::TRANSACTION, - 'database' => self::DATABASE, - 'partitionOptions' => $partitionOptions, - ], $this->expectResourceHeader(self::DATABASE, [ - self::SESSION, - self::TABLE, - new KeySet, - [ - 'transaction' => $this->transactionSelector(), - 'partitionOptions' => $partitionOptionsObj - ] - ], true, $larEnabled), null, '', $grpcConfig); - } - - public function partitionOptions() - { - return [ - [ - [], - new PartitionOptions, - true, - ['routeToLeader' => true] - ], - [ - ['maxPartitions' => 10], - new PartitionOptions([ - 'max_partitions' => 10 - ]), - true, - ['routeToLeader' => true] - ] - ]; - } - - /** - * @dataProvider keysets - */ - public function testFormatKeySet($input, $expected) - { - $this->assertEquals( - $expected, - $this->callPrivateMethod('formatKeySet', [$input]) - ); - } - - public function keysets() - { - return [ - [ - [], - [] - ], [ - [ - 'keys' => [ - [ - 1, - 2 - ] - ] - ], - [ - 'keys' => [ - $this->formatListForApi([1, 2]) - ] - ] - ], [ - [ - 'ranges' => [ - [ - 'startOpen' => [1], - 'endClosed' => [2] - ] - ], - ], [ - 'ranges' => [ - [ - 'startOpen' => $this->formatListForApi([1]), - 'endClosed' => $this->formatListForApi([2]), - ] - ] - ] - ], [ - [ - 'ranges' => [] - ], - [] - ] - ]; - } - - /** - * @dataProvider fieldvalues - */ - public function testFieldValue($input, $expected) - { - $this->assertEquals( - $expected, - $this->callPrivateMethod('fieldValue', [$input]) - ); - } - - public function fieldvalues() - { - return [ - [ - 'foo', - new Value([ - 'string_value' => 'foo' - ]) - ], [ - 1, - new Value([ - 'number_value' => 1 - ]) - ], [ - false, - new Value([ - 'bool_value' => false - ]) - ], [ - null, - new Value([ - 'null_value' => NullValue::NULL_VALUE - ]) - ], [ - [ - 'a' => 'b' - ], - new Value([ - 'struct_value' => new Struct([ - 'fields' => [ - 'a' => new Value([ - 'string_value' => 'b' - ]) - ] - ]) - ]) - ], [ - [ - 'a', 'b', 'c' - ], - new Value([ - 'list_value' => new ListValue([ - 'values' => [ - new Value([ - 'string_value' => 'a' - ]), - new Value([ - 'string_value' => 'b' - ]), - new Value([ - 'string_value' => 'c' - ]), - ] - ]) - ]) - ] - ]; - } - - /** - * @dataProvider transactionOptions - */ - public function testTransactionOptions($input, $expected) - { - // Since the tested method uses pass-by-reference arg, the callPrivateMethod function won't work. - // test on php7 only is better than nothing. - if (version_compare(PHP_VERSION, '7.0.0', '<')) { - $this->markTestSkipped('only works in php 7.'); - return; - } - - $grpc = new Grpc; - $createTransactionSelector = function () { - $args = func_get_args(); - return $this->createTransactionSelector($args[0]); - }; - - $this->assertEquals( - $expected->serializeToJsonString(), - $createTransactionSelector->call($grpc, $input)->serializeToJsonString() - ); - } - - public function transactionOptions() - { - return [ - [ - [ - 'transactionId' => self::TRANSACTION - ], - $this->transactionSelector() - ], [ - [ - 'transaction' => [ - 'singleUse' => [ - 'readWrite' => [] - ] - ] - ], - new TransactionSelector([ - 'single_use' => new TransactionOptions([ - 'read_write' => new ReadWrite - ]) - ]) - ], [ - [ - 'transaction' => [ - 'begin' => [ - 'readWrite' => [] - ] - ] - ], - new TransactionSelector([ - 'begin' => new TransactionOptions([ - 'read_write' => new ReadWrite - ]) - ]) - ] - ]; - } - - public function larOptions() - { - return [ - [ - true, - ['routeToLeader' => true] - ], [ - false, - ['routeToLeader' => false] - ] - ]; - } - - public function testPartialResultSetCustomEncoder() - { - $partialResultSet = new PartialResultSet(); - $partialResultSet->mergeFromJsonString(json_encode([ - 'metadata' => [ - 'transaction' => [ - 'id' => base64_encode(0b00010100) // bytedata is represented as a base64-encoded string in JSON - ], - 'rowType' => [ - 'fields' => [ - ['type' => ['code' => 'INT64']] // enums are represented as their string equivalents in JSON - ] - ], - ], - ])); - - $this->assertEquals(0b00010100, $partialResultSet->getMetadata()->getTransaction()->getId()); - $this->assertEquals(2, $partialResultSet->getMetadata()->getRowType()->getFields()[0]->getType()->getCode()); - - // decode the message and ensure it's decoded as expected - $grpc = new Grpc(); - $serializerProp = new \ReflectionProperty($grpc, 'serializer'); - $serializerProp->setAccessible(true); - $serializer = $serializerProp->getValue($grpc); - $arr = $serializer->encodeMessage($partialResultSet); - - // We expect this to be the binary string - $this->assertEquals(0b00010100, $arr['metadata']['transaction']['id']); - // We expect this to be the integer - $this->assertEquals(2, $arr['metadata']['rowType']['fields'][0]['type']['code']); - } - private function assertCallCorrect( - $method, - array $args, - array $expectedArgs, - $return = null, - $result = '', - $grpcConfig = [] - ) { - $this->requestWrapper->send( - Argument::type('callable'), - $expectedArgs, - Argument::type('array') - )->shouldBeCalled()->willReturn($return ?: $this->successMessage); - - $connection = new Grpc($grpcConfig); - $connection->setRequestWrapper($this->requestWrapper->reveal()); - - $this->assertEquals($result !== '' ? $result : $this->successMessage, $connection->$method($args)); - } - - /** - * Add the resource header to the args list. - * - * @param string $val The header value to add. - * @param array $args The remaining call args. - * @param boolean $append If true, should the last value in $args be an - * array, the header will be appended to that array. If false, the - * header will be added to a separate array. - * @param boolean $lar If true, will add the x-goog-spanner-route-to-leader - * header. - * @param array $options The options to add to the call. - * @return array - */ - private function expectResourceHeader( - $val, - array $args, - $append = true, - $lar = false, - $options = [] - ) { - $header = [ - 'google-cloud-resource-prefix' => [$val] - ]; - if ($lar && !isset($options['readOnly'])) { - $header['x-goog-spanner-route-to-leader'] = ['true']; - } - - $end = end($args); - if (!is_array($end) || !$append) { - $args[]['headers'] = $header; - } elseif (is_array($end)) { - $keys = array_keys($args); - $key = end($keys); - $args[$key]['headers'] = $header; - } - return $args; - } - - private function callPrivateMethod($method, array $args) - { - $grpc = new Grpc; - $ref = new \ReflectionClass($grpc); - - $method = $ref->getMethod($method); - $method->setAccessible(true); - - array_unshift($args, $grpc); - return call_user_func_array([$method, 'invoke'], $args); - } - - private function instanceConfig($full = true) - { - $args = [ - 'name' => self::CONFIG, - 'displayName' => self::CONFIG, - ]; - - if ($full) { - $args = array_merge($args, [ - 'baseConfig' => self::CONFIG, - 'configType' => InstanceConfig\Type::TYPE_UNSPECIFIED, - 'state' => State::CREATING, - 'labels' => [], - 'replicas' => [], - 'optionalReplicas' => [], - 'leaderOptions' => [], - 'reconciling' => false, - ]); - } - - $mask = []; - foreach (array_keys($args) as $key) { - if ($key != "name") { - $mask[] = Serializer::toSnakeCase($key); - } - } - - $fieldMask = $this->serializer->decodeMessage(new FieldMask, ['paths' => $mask]); - - return [ - $args, - $this->serializer->decodeMessage(new InstanceConfig, $args), - $fieldMask - ]; - } - - private function instance($full = true, $nodes = true) - { - $args = [ - 'name' => self::INSTANCE, - 'displayName' => self::INSTANCE, - ]; - - if ($full) { - if ($nodes) { - $args = array_merge($args, [ - 'config' => self::CONFIG, - 'nodeCount' => 1, - 'state' => State::CREATING, - 'labels' => [] - ]); - } else { - $args = array_merge($args, [ - 'config' => self::CONFIG, - 'processingUnits' => 1000, - 'state' => State::CREATING, - 'labels' => [] - ]); - } - } - - $mask = []; - foreach (array_keys($args) as $key) { - if ($key != "name") { - $mask[] = Serializer::toSnakeCase($key); - } - } - - $fieldMask = $this->serializer->decodeMessage(new FieldMask, ['paths' => $mask]); - - return [ - $args, - $this->serializer->decodeMessage(new Instance, $args), - $fieldMask - ]; - } - - private function transactionSelector() - { - return new TransactionSelector([ - 'id' => self::TRANSACTION - ]); - } -} - -//@codingStandardsIgnoreStart -class GrpcStub extends Grpc -{ - public $config; - - protected function constructGapic($gapicName, array $config) - { - $this->config = $config; - - return parent::constructGapic($gapicName, $config); - } -} -//@codingStandardsIgnoreEnd diff --git a/Spanner/tests/Unit/Connection/IamDatabaseTest.php b/Spanner/tests/Unit/Connection/IamDatabaseTest.php deleted file mode 100644 index 782593d18e90..000000000000 --- a/Spanner/tests/Unit/Connection/IamDatabaseTest.php +++ /dev/null @@ -1,69 +0,0 @@ -<?php -/** - * Copyright 2016 Google Inc. - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit\Connection; - -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Connection\IamDatabase; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use PHPUnit\Framework\TestCase; - -/** - * @group spanner-admin - * @group spanner - */ -class IamDatabaseTest extends TestCase -{ - use StubCreationTrait; - - private $connection; - - private $iam; - - public function setUp(): void - { - $this->connection = $this->getConnStub(); - - $this->iam = TestHelpers::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/Spanner/tests/Unit/Connection/IamInstanceTest.php b/Spanner/tests/Unit/Connection/IamInstanceTest.php deleted file mode 100644 index 6e7ff67a27aa..000000000000 --- a/Spanner/tests/Unit/Connection/IamInstanceTest.php +++ /dev/null @@ -1,69 +0,0 @@ -<?php -/** - * Copyright 2016 Google Inc. - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit\Connection; - -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Connection\IamInstance; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use PHPUnit\Framework\TestCase; - -/** - * @group spanner-admin - * @group spanner - */ -class IamInstanceTest extends TestCase -{ - use StubCreationTrait; - - private $connection; - - private $iam; - - public function setUp(): void - { - $this->connection = $this->getConnStub(); - - $this->iam = TestHelpers::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/Spanner/tests/Unit/Connection/LongRunningConnectionTest.php b/Spanner/tests/Unit/Connection/LongRunningConnectionTest.php deleted file mode 100644 index 6917e47c3ef5..000000000000 --- a/Spanner/tests/Unit/Connection/LongRunningConnectionTest.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php -/** - * Copyright 2016 Google Inc. - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit\Connection; - -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Connection\LongRunningConnection; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use PHPUnit\Framework\TestCase; - -/** - * @group spanner - * @group spanner-admin - */ -class LongRunningConnectionTest extends TestCase -{ - use StubCreationTrait; - - private $connection; - private $lro; - - public function setUp(): void - { - $this->connection = $this->getConnStub(); - $this->lro = TestHelpers::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/Spanner/tests/Unit/DatabaseTest.php b/Spanner/tests/Unit/DatabaseTest.php index 85c5c042cc37..535d1709ebc0 100644 --- a/Spanner/tests/Unit/DatabaseTest.php +++ b/Spanner/tests/Unit/DatabaseTest.php @@ -17,52 +17,78 @@ namespace Google\Cloud\Spanner\Tests\Unit; +use Google\ApiCore\OperationResponse; +use Google\ApiCore\Page; +use Google\ApiCore\PagedListResponse; use Google\ApiCore\ServerStream; use Google\ApiCore\ValidationException; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\Exception\AbortedException; use Google\Cloud\Core\Exception\NotFoundException; use Google\Cloud\Core\Exception\ServerException; use Google\Cloud\Core\Exception\ServiceException; -use Google\Cloud\Core\Iam\Iam; +use Google\Cloud\Core\Iam\IamManager; use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Core\Testing\Snippet\Fixtures; +use Google\Cloud\Spanner\Admin\Database\V1\Backup; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Database as DatabaseProto; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseDialect; -use Google\Cloud\Spanner\Connection\ConnectionInterface; -use Google\Cloud\Spanner\Connection\Grpc; +use Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseDdlResponse; +use Google\Cloud\Spanner\Admin\Database\V1\GetDatabaseRequest; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupsResponse; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\Database; -use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\Snapshot; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; +use Google\Cloud\Spanner\V1\BatchWriteRequest; +use Google\Cloud\Spanner\V1\BatchWriteRequest\MutationGroup; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CommitRequest; +use Google\Cloud\Spanner\V1\CommitResponse; +use Google\Cloud\Spanner\V1\DeleteSessionRequest; use Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type as ReplicaType; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlRequest; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlResponse; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\Mutation; +use Google\Cloud\Spanner\V1\PartialResultSet; +use Google\Cloud\Spanner\V1\ReadRequest; +use Google\Cloud\Spanner\V1\ReadRequest\LockHint; +use Google\Cloud\Spanner\V1\ReadRequest\OrderBy; use Google\Cloud\Spanner\V1\ResultSet; +use Google\Cloud\Spanner\V1\ResultSetMetadata; use Google\Cloud\Spanner\V1\ResultSetStats; use Google\Cloud\Spanner\V1\Session as SessionProto; -use Google\Cloud\Spanner\V1\SpannerClient; +use Google\Cloud\Spanner\V1\StructType; +use Google\Cloud\Spanner\V1\StructType\Field; use Google\Cloud\Spanner\V1\Transaction as TransactionProto; -use Google\Cloud\Spanner\V1\TransactionOptions; -use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode as ReadLockMode; +use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; +use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode; +use Google\Cloud\Spanner\V1\TransactionSelector; +use Google\Cloud\Spanner\V1\Type as TypeProto; +use Google\LongRunning\Client\OperationsClient; +use Google\Protobuf\Duration; +use Google\Protobuf\ListValue; +use Google\Protobuf\Timestamp as TimestampProto; +use Google\Protobuf\Value; use Google\Rpc\Code; +use Google\Rpc\Status; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; -use InvalidArgumentException; -use Google\Cloud\Spanner\V1\ReadRequest\LockHint; -use Google\Cloud\Spanner\V1\ReadRequest\OrderBy; /** * @group spanner @@ -71,10 +97,9 @@ class DatabaseTest extends TestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; use ResultGeneratorTrait; - use StubCreationTrait; + use ApiHelperTrait; const PROJECT = 'my-awesome-project'; const DATABASE = 'my-database'; @@ -87,70 +112,73 @@ class DatabaseTest extends TestCase const TIMESTAMP = '2017-01-09T18:05:22.534799Z'; const BEGIN_RW_OPTIONS = ['begin' => ['readWrite' => [], 'isolationLevel' => 0]]; - private $connection; + private const DIRECTED_READ_OPTIONS_INCLUDE_REPLICAS = [ + 'includeReplicas' => [ + 'autoFailoverDisabled' => false, + 'replicaSelections' => [ + [ + 'location' => 'us-central1', + 'type' => ReplicaType::READ_WRITE, + ] + ] + ] + ]; + + private const DIRECTED_READ_OPTIONS_EXCLUDE_REPLICAS = [ + 'excludeReplicas' => [ + 'replicaSelections' => [ + [ + 'location' => 'us-central1', + 'type' => ReplicaType::READ_WRITE, + ] + ] + ] + ]; + + private $spannerClient; + private $instanceAdminClient; + private $databaseAdminClient; + private $serializer; private $instance; private $sessionPool; - private $lro; - private $lroCallables; private $database; private $session; private $databaseWithDatabaseRole; private $directedReadOptionsIncludeReplicas; private $directedReadOptionsExcludeReplicas; + private $operationResponse; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->prophesize(ConnectionInterface::class); + $this->serializer = new Serializer(); $this->sessionPool = $this->prophesize(SessionPoolInterface::class); - $this->lro = $this->prophesize(LongRunningConnectionInterface::class); - $this->lroCallables = []; - $this->session = TestHelpers::stub(Session::class, [ - $this->connection->reveal(), + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->instanceAdminClient = $this->prophesize(InstanceAdminClient::class); + $this->databaseAdminClient = $this->prophesize(DatabaseAdminClient::class); + $this->databaseAdminClient->getOperationsClient() + ->willReturn($this->prophesize(OperationsClient::class)); + + $this->session = new Session( + $this->spannerClient->reveal(), + $this->serializer, self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION - ]); - $this->directedReadOptionsIncludeReplicas = [ - 'includeReplicas' => [ - 'autoFailoverDisabled' => false, - 'replicaSelections' => [ - [ - 'location' => 'us-central1', - 'type' => ReplicaType::READ_WRITE, - - ] - ] - ] - ]; - $this->directedReadOptionsExcludeReplicas = [ - 'excludeReplicas' => [ - 'autoFailoverDisabled' => false, - 'replicaSelections' => [ - [ - 'location' => 'us-central1', - 'type' => ReplicaType::READ_WRITE, - ] - ] - ] - ]; + ); - $this->instance = TestHelpers::stub(Instance::class, [ - $this->connection->reveal(), - $this->lro->reveal(), - $this->lroCallables, + $this->instance = new Instance( + $this->spannerClient->reveal(), + $this->instanceAdminClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, self::PROJECT, self::INSTANCE, - false, - [], - ['directedReadOptions' => $this->directedReadOptionsIncludeReplicas] - ], [ - 'info', - 'connection' - ]); + ['directedReadOptions' => self::DIRECTED_READ_OPTIONS_INCLUDE_REPLICAS] + ); $this->sessionPool->acquire(Argument::type('string')) ->willReturn($this->session); @@ -159,26 +187,20 @@ public function setUp(): void $this->sessionPool->release(Argument::type(Session::class)) ->willReturn(null); - $args = [ - $this->connection->reveal(), + $this->database = new Database( + $this->spannerClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, $this->instance, - $this->lro->reveal(), - $this->lroCallables, self::PROJECT, self::DATABASE, - $this->sessionPool->reveal(), - false, - [], - 'Reader' - ]; - - $props = [ - 'connection', 'operation', 'session', 'sessionPool', 'instance' - ]; + [ + 'sessionPool' => $this->sessionPool->reveal(), + 'databaseRole' => 'Reader', + ] + ); - $this->database = TestHelpers::stub(Database::class, $args, $props); - $args[6] = null; - $this->databaseWithDatabaseRole = TestHelpers::stub(Database::class, $args, $props); + $this->operationResponse = $this->prophesize(OperationResponse::class); } public function testName() @@ -191,17 +213,17 @@ public function testName() public function testInfo() { - $res = [ - 'name' => $this->database->name() - ]; - - $this->connection->getDatabase(Argument::withEntry('name', $this->database->name())) - ->shouldBeCalledTimes(1) - ->willReturn($res); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabase( + Argument::that(function (GetDatabaseRequest $request) { + return $request->getName() === $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new DatabaseProto(['name' => $this->database->name()])); - $this->assertEquals($res, $this->database->info()); + $this->assertArrayHasKey('name', $this->database->info()); + $this->assertEquals($this->database->info()['name'], $this->database->name()); // Make sure the request only is sent once. $this->database->info(); @@ -212,11 +234,14 @@ public function testState() $res = [ 'state' => Database::STATE_READY ]; - $this->connection->getDatabase(Argument::withEntry('name', $this->database->name())) - ->shouldBeCalledTimes(1) - ->willReturn($res); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabase( + Argument::that(function (GetDatabaseRequest $request) { + return $request->getName() === $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new DatabaseProto($res)); $this->assertEquals(Database::STATE_READY, $this->database->state()); @@ -227,18 +252,22 @@ public function testState() public function testCreateBackup() { $expireTime = new \DateTime(); - $this->connection->createBackup(Argument::allOf( - Argument::withEntry('instance', $this->instance->name()), - Argument::withEntry('backupId', self::BACKUP), - Argument::withEntry('backup', [ - 'database' => $this->database->name(), - 'expireTime' => $expireTime->format('Y-m-d\TH:i:s.u\Z') - ]) - )) - ->shouldBeCalled() - ->willReturn(['name' => 'operations/foo']); - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->createBackup( + Argument::that(function ($request) use ($expireTime) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['parent'], + $this->instance->name() + ); + $this->assertEquals($message['backupId'], self::BACKUP); + return $message['backup']['expireTime'] == $expireTime->format('Y-m-d\TH:i:s.u\Z') + && $message['backup']['database'] == $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $op = $this->database->createBackup(self::BACKUP, $expireTime); @@ -248,27 +277,36 @@ public function testCreateBackup() public function testBackups() { $backups = [ - [ - 'name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, 'backup1'), - ], - [ - 'name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, 'backup2'), - ] + new Backup(['name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, 'backup1')]), + new Backup(['name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, 'backup2')]) ]; - $expectedFilter = 'database:' . $this->database->name(); - $this->connection->listBackups(Argument::withEntry('filter', $expectedFilter)) - ->shouldBeCalled() - ->willReturn(['backups' => $backups]); + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListBackupsResponse(['backups' => $backups])); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $expectedFilter = 'database:' . $this->database->name(); + $this->databaseAdminClient->listBackups( + Argument::that(function ($request) use ($expectedFilter) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['filter'], + $expectedFilter + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($pagedListResponse->reveal()); $bkps = $this->database->backups(); - $this->assertInstanceOf(ItemIterator::class, $bkps); $bkps = iterator_to_array($bkps); - $this->assertCount(2, $bkps); $this->assertEquals('backup1', DatabaseAdminClient::parseName($bkps[0]->name())['backup']); $this->assertEquals('backup2', DatabaseAdminClient::parseName($bkps[1]->name())['backup']); @@ -276,23 +314,34 @@ public function testBackups() public function testBackupsWithCustomFilter() { - $backups = [ - [ - 'name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, 'backup1'), - ], - [ - 'name' => DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, 'backup2'), - ] - ]; + $backup1 = DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, 'backup1'); + $backup2 = DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, 'backup2'); + $backups = [new Backup(['name' => $backup1]), new Backup(['name' => $backup2])]; + + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListBackupsResponse(['backups' => $backups])); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + $defaultFilter = 'database:' . $this->database->name(); $customFilter = 'customFilter'; $expectedFilter = sprintf('(%1$s) AND (%2$s)', $defaultFilter, $customFilter); - $this->connection->listBackups(Argument::withEntry('filter', $expectedFilter)) - ->shouldBeCalled() - ->willReturn(['backups' => $backups]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->listBackups( + Argument::that(function ($request) use ($expectedFilter) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['filter'], + $expectedFilter + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($pagedListResponse->reveal()); $bkps = $this->database->backups(['filter' => $customFilter]); @@ -311,13 +360,18 @@ public function testReload() 'name' => $this->database->name() ]; - $this->connection->getDatabase(Argument::withEntry('name', $this->database->name())) + $this->databaseAdminClient->getDatabase( + Argument::that(function (GetDatabaseRequest $request) { + return $request->getName() === $this->database->name(); + }), + Argument::type('array') + ) ->shouldBeCalledTimes(2) - ->willReturn($res); + ->willReturn(new DatabaseProto($res)); - $this->database->___setProperty('connection', $this->connection->reveal()); - - $this->assertEquals($res, $this->database->reload()); + $info = $this->database->reload(); + $this->assertArrayHasKey('name', $info); + $this->assertEquals($info['name'], $this->database->name()); // Make sure the request is sent each time the method is called. $this->database->reload(); @@ -328,12 +382,14 @@ public function testReload() */ public function testExists() { - $this->connection->getDatabase(Argument::withEntry( - 'name', - DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) - ))->shouldBeCalled()->willReturn([]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabase( + Argument::that(function (GetDatabaseRequest $request) { + return $request->getName() === $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new DatabaseProto()); $this->assertTrue($this->database->exists()); } @@ -343,12 +399,15 @@ public function testExists() */ public function testExistsNotFound() { - $this->connection->getDatabase(Argument::withEntry('name', $this->database->name())) - ->shouldBeCalled() + $this->databaseAdminClient->getDatabase( + Argument::that(function (GetDatabaseRequest $request) { + return $request->getName() === $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() ->willThrow(new NotFoundException('', 404)); - $this->database->___setProperty('connection', $this->connection->reveal()); - $this->assertFalse($this->database->exists()); } @@ -357,16 +416,27 @@ public function testExistsNotFound() */ public function testCreate() { - $this->connection->createDatabase(Argument::allOf( - Argument::withEntry('createStatement', 'CREATE DATABASE `my-database`'), - Argument::withEntry('extraStatements', [ - 'CREATE TABLE bar' - ]) - ))->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->createDatabase( + Argument::that(function ($request) { + $createStatement = $request->getCreateStatement(); + $extraStatements = $request->getExtraStatements(); + $this->assertStringContainsString('my-database', $createStatement); + $this->assertEquals(['CREATE TABLE bar'], iterator_to_array($extraStatements)); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); + + new OperationResponse('my-operation', new DatabaseAdminClient([ + 'credentials' => Fixtures::KEYFILE_STUB_FIXTURE() + ]), [ + 'lastProtoResponse' => $this->serializer->decodeMessage( + new DatabaseProto(), + ['name' => 'my-database'] + ) + ]); $op = $this->database->create([ 'statements' => [ @@ -382,20 +452,23 @@ public function testCreate() */ public function testUpdateDatabase() { - $this->connection->updateDatabase(Argument::allOf( - Argument::withEntry('database', [ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE), - 'enableDropProtection' => true, - ]), - Argument::withEntry('updateMask', ['paths' => ['enable_drop_protection']]) - ))->shouldBeCalledTimes(1)->willReturn([ - 'enableDropProtection' => true - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->updateDatabase( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['database']['name'], + DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + $this->assertEquals($message['updateMask'], ['paths' => ['enable_drop_protection']]); + return $message['database']['enableDropProtection']; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); - $res = $this->database->updateDatabase(['enableDropProtection' => true]); - $this->assertTrue($res['enableDropProtection']); + $op = $this->database->updateDatabase(['enableDropProtection' => true]); + $this->assertInstanceOf(LongRunningOperation::class, $op); } /** @@ -405,14 +478,17 @@ public function testCreatePostgresDialect() { $createStatement = sprintf('CREATE DATABASE "%s"', self::DATABASE); - $this->connection->createDatabase(Argument::allOf( - Argument::withEntry('createStatement', $createStatement), - Argument::withEntry('extraStatements', []) - ))->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->createDatabase( + Argument::that(function ($request) use ($createStatement) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['createStatement'], $createStatement); + $this->assertEmpty($message['extraStatements']); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $op = $this->database->create([ 'databaseDialect' => DatabaseDialect::POSTGRESQL @@ -427,17 +503,22 @@ public function testCreatePostgresDialect() public function testRestoreFromBackupName() { $backupName = DatabaseAdminClient::backupName(self::PROJECT, self::INSTANCE, self::BACKUP); - $this->connection->restoreDatabase(Argument::allOf( - Argument::withEntry('instance', $this->instance->name()), - Argument::withEntry('databaseId', self::DATABASE), - Argument::withEntry('backup', $backupName) - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->restoreDatabase( + Argument::that(function ($request) use ($backupName) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['parent'], + $this->instance->name() + ); + $this->assertEquals($message['databaseId'], self::DATABASE); + $this->assertEquals($message['backup'], $backupName); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $op = $this->database->restore($backupName); $this->assertInstanceOf(LongRunningOperation::class, $op); @@ -450,17 +531,21 @@ public function testRestoreFromBackupObject() { $backupObj = $this->instance->backup(self::BACKUP); - $this->connection->restoreDatabase(Argument::allOf( - Argument::withEntry('instance', $this->instance->name()), - Argument::withEntry('databaseId', self::DATABASE), - Argument::withEntry('backup', $backupObj->name()) - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->restoreDatabase( + Argument::that(function ($request) use ($backupObj) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['parent'], + $this->instance->name() + ); + $this->assertEquals($message['databaseId'], self::DATABASE); + $this->assertEquals($message['backup'], $backupObj->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $op = $this->database->restore($backupObj); $this->assertInstanceOf(LongRunningOperation::class, $op); @@ -472,34 +557,46 @@ public function testRestoreFromBackupObject() public function testUpdateDdl() { $statement = 'foo'; - $this->connection->updateDatabaseDdl([ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE), - 'statements' => [$statement] - ])->willReturn([ - 'name' => 'my-operation' - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->updateDatabaseDdl( + Argument::that(function ($request) use ($statement) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['database'], + DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + $this->assertEquals($message['statements'], [$statement]); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $this->database->updateDdl($statement); $this->assertInstanceOf(LongRunningOperation::class, $res); } - /** * @group spanner-admin */ public function testUpdateDdlBatch() { $statements = ['foo', 'bar']; - $this->connection->updateDatabaseDdl([ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE), - 'statements' => $statements - ])->willReturn([ - 'name' => 'my-operation' - ])->shouldBeCalled(); - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->updateDatabaseDdl( + Argument::that(function ($request) use ($statements) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['database'], + DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + $this->assertEquals($message['statements'], $statements); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $this->database->updateDdlBatch($statements); } @@ -510,12 +607,21 @@ public function testUpdateDdlBatch() public function testUpdateWithSingleStatement() { $statement = 'foo'; - $this->connection->updateDatabaseDdl([ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE), - 'statements' => ['foo'] - ])->shouldBeCalled()->willReturn(['name' => 'operations/foo']); - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->updateDatabaseDdl( + Argument::that(function ($request) use ($statement) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['database'], + DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + $this->assertEquals($message['statements'], [$statement]); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $res = $this->database->updateDdl($statement); $this->assertInstanceOf(LongRunningOperation::class, $res); @@ -526,14 +632,21 @@ public function testUpdateWithSingleStatement() */ public function testDrop() { - $this->connection->dropDatabase([ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) - ])->shouldBeCalled(); + $this->databaseAdminClient->dropDatabase( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['database'], + DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); $this->sessionPool->clear()->shouldBeCalled()->willReturn(null); - $this->database->___setProperty('connection', $this->connection->reveal()); - $this->database->drop(); } @@ -542,53 +655,56 @@ public function testDrop() */ public function testDropDeleteSession() { - $this->connection->createSession(Argument::withEntry('database', $this->database->name())) - ->shouldBeCalled() - ->willReturn([ - 'name' => $this->session->name() - ]); + $this->spannerClient->createSession( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['database'] == $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new SessionProto(['name' => $this->session->name()])); - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) - ->shouldBeCalled() - ->willReturn([ - 'id' => self::TRANSACTION - ]); + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $this->connection->deleteSession(Argument::allOf( - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ), - Argument::withEntry('name', $this->session->name()) - )) - ->shouldBeCalled(); + $this->spannerClient->deleteSession( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['name'] == $this->session->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); - $this->connection->dropDatabase([ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) - ])->shouldBeCalled(); + $this->databaseAdminClient->dropDatabase( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['database'], + DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); - $database = TestHelpers::stub(Database::class, [ - $this->connection->reveal(), + $database = new Database( + $this->spannerClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, $this->instance, - $this->lro->reveal(), - $this->lroCallables, self::PROJECT, self::DATABASE - ]); + ); // This will set a session on the Database class. $database->transaction(); @@ -602,11 +718,19 @@ public function testDropDeleteSession() public function testDdl() { $ddl = ['create table users', 'create table posts']; - $this->connection->getDatabaseDDL([ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) - ])->willReturn(['statements' => $ddl]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabaseDdl( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['database'], + DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new GetDatabaseDdlResponse(['statements' => $ddl])); $this->assertEquals($ddl, $this->database->ddl()); } @@ -616,11 +740,19 @@ public function testDdl() */ public function testDdlNoResult() { - $this->connection->getDatabaseDDL([ - 'name' => DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) - ])->willReturn([]); - - $this->database->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabaseDdl( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['database'], + DatabaseAdminClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new GetDatabaseDdlResponse()); $this->assertEquals([], $this->database->ddl()); } @@ -630,26 +762,20 @@ public function testDdlNoResult() */ public function testIam() { - $this->assertInstanceOf(Iam::class, $this->database->iam()); + $this->assertInstanceOf(IamManager::class, $this->database->iam()); } public function testSnapshot() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) - ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $res = $this->database->snapshot(); $this->assertInstanceOf(Snapshot::class, $res); @@ -676,13 +802,9 @@ public function testSnapshotNestedTransaction() // Begin transaction RPC is skipped when begin is inlined // and invoked only if `begin` fails or if commit is the // sole operation in the transaction. - $this->connection->beginTransaction(Argument::any()) - ->shouldNotBeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); - $this->connection->rollback(Argument::any()) - ->shouldNotBeCalled(); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->rollback(Argument::cetera())->shouldNotBeCalled(); $this->database->runTransaction(function ($t) { $this->database->snapshot(); @@ -691,27 +813,26 @@ public function testSnapshotNestedTransaction() public function testBatchWrite() { - $expectedMutationGroup = ['mutations' => [ - [ - Operation::OP_INSERT_OR_UPDATE => [ - 'table' => 'foo', - 'columns' => ['bar1', 'bar2'], - 'values' => [1, 2] - ] - ] - ]]; - $this->connection->batchWrite(Argument::allOf( - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ), - Argument::withEntry('session', $this->session->name()), - Argument::withEntry('mutationGroups', [$expectedMutationGroup]) - ))->shouldBeCalled()->willReturn(['foo result']); + $expectedMutationGroup = new MutationGroup(['mutations' => [ + new Mutation(['insert_or_update' => new Mutation\Write([ + 'table' => 'foo', + 'columns' => ['bar1', 'bar2'], + 'values' => [new ListValue(['values' => [ + new Value(['string_value' => '1']), + new Value(['string_value' => '2']), + ]])] + ])]) + ]]); + + $this->spannerClient->batchWrite( + Argument::that(function ($request) use ($expectedMutationGroup) { + return $request->getSession() === $this->session->name() + && $request->getMutationGroups()[0] == $expectedMutationGroup; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $mutationGroups = [ ($this->database->mutationGroup(false)) @@ -721,49 +842,13 @@ public function testBatchWrite() ) ]; - $this->refreshOperation($this->database, $this->connection->reveal()); - $result = $this->database->batchWrite($mutationGroups); - $this->assertIsArray($result); + $this->assertEquals('10', iterator_to_array($result)[0]['values'][0]); } public function testRunTransaction() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - ]) - )) - ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); - - $this->connection->commit(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - ]) - )) - ->shouldBeCalled() - ->willReturn(['commitTimestamp' => '2017-01-09T18:05:22.534799Z']); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->stubCommit(false); $hasTransaction = false; @@ -780,12 +865,9 @@ public function testRunTransactionNoCommit() { $this->expectException(\InvalidArgumentException::class); - $this->connection->beginTransaction(Argument::any()) - ->shouldNotBeCalled(); - - $this->connection->rollback(Argument::any())->shouldNotBeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->rollback(Argument::cetera())->shouldNotBeCalled(); $this->database->runTransaction($this->noop()); } @@ -794,13 +876,9 @@ public function testRunTransactionNestedTransaction() { $this->expectException(\BadMethodCallException::class); - $this->connection->beginTransaction(Argument::any()) - ->shouldNotBeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); - $this->connection->rollback(Argument::any()) - ->shouldNotBeCalled(); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->rollback(Argument::cetera())->shouldNotBeCalled(); $this->database->runTransaction(function ($t) { $this->database->runTransaction($this->noop()); @@ -813,23 +891,19 @@ public function testRunTransactionShouldRetryOnRstStreamErrors() $this->expectExceptionMessage('RST_STREAM'); $err = new ServerException('RST_STREAM', Code::INTERNAL); - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalledTimes(3) ->willThrow($err); $this->database->runTransaction(function ($t) { $t->commit(); - }, ['maxRetries' => 2]); + }, ['retrySettings' => ['maxRetries' => 2]]); } public function testRunTransactionRetry() @@ -843,51 +917,41 @@ public function testRunTransactionRetry() ] ]); - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalledTimes(3) - ->willReturn(['id' => self::TRANSACTION]); + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $it = 0; - $this->connection->commit(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) + $commitResponse = $this->commitResponse(); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalledTimes(3) - ->will(function () use (&$it, $abort) { + ->will(function () use (&$it, $abort, $commitResponse) { $it++; if ($it <= 2) { throw $abort; } - return ['commitTimestamp' => TransactionTest::TIMESTAMP]; + return $commitResponse; }); - $this->refreshOperation($this->database, $this->connection->reveal()); - $this->database->runTransaction(function ($t) use (&$it) { if ($it > 0) { $this->assertTrue($t->isRetry()); } else { $this->assertFalse($t->isRetry()); } - $t->commit(); }); } @@ -905,45 +969,33 @@ public function testRunTransactionAborted() ] ]); - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalledTimes(Database::MAX_RETRIES + 1) - ->willReturn(['id' => self::TRANSACTION]); + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $it = 0; - $this->connection->commit(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalledTimes(Database::MAX_RETRIES + 1) ->will(function () use (&$it, $abort) { $it++; - if ($it <= Database::MAX_RETRIES + 1) { throw $abort; } - return ['commitTimestamp' => TransactionTest::TIMESTAMP]; }); - $this->refreshOperation($this->database, $this->connection->reveal()); - $this->database->runTransaction(function ($t) { $t->commit(); }); @@ -951,24 +1003,19 @@ public function testRunTransactionAborted() public function testTransaction() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - ]) - )) - ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['requestOptions']['transactionTag' ], + self::TRANSACTION_TAG, + ); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $t = $this->database->transaction(['tag' => self::TRANSACTION_TAG]); $this->assertInstanceOf(Transaction::class, $t); @@ -976,32 +1023,23 @@ public function testTransaction() public function testTransactionWithIsolationLevel() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - ]), - Argument::withEntry('transactionOptions', [ - 'readWrite' => [], - 'isolationLevel' => IsolationLevel::REPEATABLE_READ, - ]) - )) + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertNotNull($txnOptions = $request->getOptions()); + $this->assertEquals( + IsolationLevel::REPEATABLE_READ, + $txnOptions->getIsolationLevel() + ); + return true; + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $t = $this->database->transaction([ 'tag' => self::TRANSACTION_TAG, - 'isolationLevel' => IsolationLevel::REPEATABLE_READ + 'transactionOptions' => ['isolationLevel' => IsolationLevel::REPEATABLE_READ], ]); $this->assertInstanceOf(Transaction::class, $t); } @@ -1010,13 +1048,9 @@ public function testTransactionNestedTransaction() { $this->expectException(\BadMethodCallException::class); - $this->connection->beginTransaction(Argument::any()) - ->shouldNotBeCalled(); - - $this->connection->rollback(Argument::any()) - ->shouldNotBeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->rollback(Argument::cetera())->shouldNotBeCalled(); $this->database->runTransaction(function ($t) { $this->database->transaction(); @@ -1028,23 +1062,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $row) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][OPERATION::OP_INSERT]['values'][0][0] !== current($row)) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->insert($table, $row); $this->assertInstanceOf(Timestamp::class, $res); @@ -1056,23 +1095,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $row) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][OPERATION::OP_INSERT]['values'][0][0] !== current($row)) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->insertBatch($table, [$row]); $this->assertInstanceOf(Timestamp::class, $res); @@ -1084,23 +1128,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $row) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_UPDATE]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][OPERATION::OP_UPDATE]['values'][0][0] !== current($row)) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->update($table, $row); $this->assertInstanceOf(Timestamp::class, $res); @@ -1112,23 +1161,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $row) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_UPDATE]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][OPERATION::OP_UPDATE]['values'][0][0] !== current($row)) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->updateBatch($table, [$row]); $this->assertInstanceOf(Timestamp::class, $res); @@ -1140,23 +1194,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $row) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_INSERT_OR_UPDATE]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][OPERATION::OP_INSERT_OR_UPDATE]['values'][0][0] !== current($row)) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->insertOrUpdate($table, $row); $this->assertInstanceOf(Timestamp::class, $res); @@ -1168,23 +1227,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $row) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_INSERT_OR_UPDATE]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][OPERATION::OP_INSERT_OR_UPDATE]['values'][0][0] !== current($row)) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->insertOrUpdateBatch($table, [$row]); $this->assertInstanceOf(Timestamp::class, $res); @@ -1196,23 +1260,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $row) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_REPLACE]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][OPERATION::OP_REPLACE]['values'][0][0] !== current($row)) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->replace($table, $row); $this->assertInstanceOf(Timestamp::class, $res); @@ -1224,23 +1293,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $row) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_REPLACE]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) { - return false; - } + if ($request['mutations'][0][OPERATION::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][OPERATION::OP_REPLACE]['values'][0][0] !== current($row)) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->replaceBatch($table, [$row]); $this->assertInstanceOf(Timestamp::class, $res); @@ -1252,23 +1326,28 @@ 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; - } + $this->spannerClient->commit( + Argument::that(function ($request) use ($table, $keys) { + $request = $this->serializer->encodeMessage($request); - if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][0] !== (string) $keys[0]) { - return false; - } + if ($request['mutations'][0][Operation::OP_DELETE]['table'] !== $table) { + return false; + } - if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][1] !== $keys[1]) { - return false; - } + if ($request['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][0][0] !== (string) $keys[0]) { + return false; + } - return true; - }))->shouldBeCalled()->willReturn($this->commitResponse()); + if ($request['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][1][0] !== $keys[1]) { + return false; + } - $this->refreshOperation($this->database, $this->connection->reveal()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $res = $this->database->delete($table, new KeySet(['keys' => $keys])); $this->assertInstanceOf(Timestamp::class, $res); @@ -1279,12 +1358,22 @@ public function testExecute() { $sql = 'SELECT * FROM Table'; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('headers', ['x-goog-spanner-route-to-leader' => ['true']]) - ))->shouldBeCalled()->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) use ($sql) { + return $request->getSql() == $sql; + }), + Argument::that(function ($callOptions) { + $this->assertArrayHasKey('route-to-leader', $callOptions); + $this->assertEquals(true, $callOptions['route-to-leader']); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream( + null, + new ResultSetStats(['row_count_lower_bound' => 1]), + self::TRANSACTION + )); $res = $this->database->execute($sql, [ 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE @@ -1298,18 +1387,21 @@ public function testExecuteWithIsolationLevel() { $sql = 'SELECT * FROM Table'; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('headers', ['x-goog-spanner-route-to-leader' => ['true']]), - Argument::withEntry('transaction', [ - 'begin' => [ - 'readWrite' => [], - 'isolationLevel' => IsolationLevel::REPEATABLE_READ - ] - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $this->assertEquals($sql, $request->getSql()); + $this->assertNotNull($txnOptions = $request->getTransaction()->getBegin()); + $this->assertEquals(IsolationLevel::REPEATABLE_READ, $txnOptions->getIsolationLevel()); + return true; + }), + Argument::that(function ($callOptions) { + $this->assertArrayHasKey('route-to-leader', $callOptions); + $this->assertEquals(true, $callOptions['route-to-leader']); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $res = $this->database->execute($sql, [ 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE, @@ -1324,16 +1416,17 @@ public function testExecuteWithIsolationLevel() public function testExecuteWithSingleSession() { - $this->database->___setProperty('sessionPool', null); - $this->database->___setProperty('session', $this->session); $sql = 'SELECT * FROM Table'; $sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION); - $this->connection->executeStreamingSql(Argument::withEntry('session', $sessName)) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) use ($sessName) { + return $request->getSession() == $sessName; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $res = $this->database->execute($sql); $rows = iterator_to_array($res->rows()); @@ -1341,19 +1434,20 @@ public function testExecuteWithSingleSession() public function testExecuteSingleUseMaxStaleness() { - $this->database->___setProperty('sessionPool', null); - $this->database->___setProperty('session', $this->session); $sql = 'SELECT * FROM Table'; $sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION); - $this->connection->executeStreamingSql(Argument::withEntry('session', $sessName)) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) use ($sessName) { + return $request->getSession() == $sessName; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $res = $this->database->execute($sql, [ - 'maxStaleness' => new Duration(10, 0) + 'maxStaleness' => new Duration(['seconds' => 10, 'nanos' => 0]) ]); $rows = iterator_to_array($res->rows()); } @@ -1362,35 +1456,51 @@ public function testExecuteBeginMaxStalenessFails() { $this->expectException(\BadMethodCallException::class); - $this->database->___setProperty('sessionPool', null); - $this->database->___setProperty('session', $this->session); $sql = 'SELECT * FROM Table'; $this->database->execute($sql, [ 'begin' => true, - 'maxStaleness' => new Duration(10, 0) + 'maxStaleness' => new Duration(['seconds' => 10, 'nanos' => 0]) ]); } public function testExecutePartitionedUpdate() { $sql = 'UPDATE foo SET bar = @bar'; - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('transactionOptions', [ - 'partitionedDml' => [] - ]), - Argument::withEntry('singleUse', false) - ))->shouldBeCalled()->willReturn([ - 'id' => self::TRANSACTION - ]); + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['options']['partitionedDml' ], + [] + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', ['id' => self::TRANSACTION]), - Argument::withEntry('headers', ['x-goog-spanner-route-to-leader' => ['true']]) - ))->shouldBeCalled()->willReturn($this->resultGenerator(true)); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['sql'], $sql); + $this->assertEquals($message['transaction'], ['id' => self::TRANSACTION]); + return true; + }), + Argument::that(function ($callOptions) { + $this->assertArrayHasKey('route-to-leader', $callOptions); + $this->assertEquals(true, $callOptions['route-to-leader']); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream( + null, + new ResultSetStats(['row_count_lower_bound' => 1]), + true + )); - $this->refreshOperation($this->database, $this->connection->reveal()); $res = $this->database->executePartitionedUpdate($sql); $this->assertEquals(1, $res); @@ -1400,7 +1510,6 @@ public function testExecutePartitionedUpdateWithIsolationLevelShouldRaise() { $sql = 'UPDATE foo SET bar = @bar'; - $this->refreshOperation($this->database, $this->connection->reveal()); $this->expectException(ValidationException::class); $res = $this->database->executePartitionedUpdate($sql, [ @@ -1417,27 +1526,21 @@ public function testRead() $table = 'Table'; $opts = ['foo' => 'bar']; - $this->connection->streamingRead(Argument::that(function ($arg) use ($table) { - if ($arg['table'] !== $table) { - return false; - } - - if ($arg['keySet']['all'] !== true) { - return false; - } - - if ($arg['columns'] !== ['ID']) { - return false; - } - - if ($arg['headers'] !== ['x-goog-spanner-route-to-leader' => ['true']]) { - return false; - } - - return true; - }))->shouldBeCalled()->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::that(function (ReadRequest $request) use ($table) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($request->getTable(), $table); + $this->assertEquals( + $message['keySet'], + ['all' => true, 'keys' => [], 'ranges' => []] + ); + $this->assertEquals($message['columns'], ['ID']); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $res = $this->database->read( $table, @@ -1450,16 +1553,20 @@ public function testRead() $this->assertEquals(10, $rows[0]['ID']); } - public function testSetOrderByReachesTheConnection() + public function testSetOrderBy() { $table = 'Table'; $opts = ['foo' => 'bar']; - $this->connection->streamingRead(Argument::withEntry('orderBy', OrderBy::ORDER_BY_PRIMARY_KEY)) + $this->spannerClient->streamingRead( + Argument::that(function (ReadRequest $request) { + $this->assertEquals(OrderBy::ORDER_BY_PRIMARY_KEY, $request->getOrderBy()); + return true; + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + ->willReturn($this->resultGeneratorStream()); $options = [ 'orderBy' => OrderBy::ORDER_BY_PRIMARY_KEY @@ -1476,16 +1583,20 @@ public function testSetOrderByReachesTheConnection() $this->assertEquals(10, $rows[0]['ID']); } - public function testSetLockHintReachesTheConnection() + public function testSetLockHint() { $table = 'Table'; $opts = ['foo' => 'bar']; - $this->connection->streamingRead(Argument::withEntry('lockHint', LockHint::LOCK_HINT_SHARED)) + $this->spannerClient->streamingRead( + Argument::that(function (ReadRequest $request) { + $this->assertEquals(LockHint::LOCK_HINT_SHARED, $request->getLockHint()); + return true; + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->database, $this->connection->reveal()); + ->willReturn($this->resultGeneratorStream()); $options = [ 'lockHint' => LockHint::LOCK_HINT_SHARED @@ -1509,57 +1620,82 @@ public function testSessionPool() public function testClose() { + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); + $this->sessionPool->release(Argument::type(Session::class)) ->shouldBeCalled() ->willReturn(null); - $this->database->___setProperty('sessionPool', $this->sessionPool->reveal()); - $this->database->___setProperty('session', $this->session); + // start a transaction to create a session + $this->database->transaction(); $this->database->close(); - - $this->assertNull($this->database->___getProperty('session')); } public function testCloseNoPool() { - $this->connection->deleteSession(Argument::allOf( - Argument::withEntry('name', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) - ->shouldBeCalled() - ->willReturn([]); + $database = new Database( + $this->spannerClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance, + self::PROJECT, + self::DATABASE + ); + + $this->spannerClient->createSession( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['database'] == $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new SessionProto(['name' => $this->session->name()])); + + $this->spannerClient->deleteSession( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['name'] == $this->session->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); + + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $this->session->___setProperty('connection', $this->connection->reveal()); - $this->database->___setProperty('sessionPool', null); - $this->database->___setProperty('session', $this->session); + // start a transaction to create a session + $database->transaction(); $this->database->close(); } public function testCreateSession() { - $db = SpannerClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE); - $sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION); - $this->connection->createSession(Argument::withEntry('database', $db)) - ->shouldBeCalled() - ->willReturn([ - 'name' => $sessName - ]); - - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->createSession( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['database'] == $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new SessionProto(['name' => $this->session->name()])); $sess = $this->database->createSession(); $this->assertInstanceOf(Session::class, $sess); - $this->assertEquals($sessName, $sess->name()); + $this->assertEquals($this->session->name(), $sess->name()); } public function testSession() @@ -1584,22 +1720,22 @@ public function testIdentity() ], $this->database->identity()); } - public function testConnection() - { - $this->assertInstanceOf(ConnectionInterface::class, $this->database->connection()); - } - // ******* // Helpers private function commitResponse() { - return ['commitTimestamp' => '2017-01-09T18:05:22.534799Z']; + return new CommitResponse([ + 'commit_timestamp' => new TimestampProto([ + 'seconds' => (new \DateTime(self::TIMESTAMP))->format('U'), + 'nanos' => 534799000 + ]) + ]); } private function assertTimestampIsCorrect($res) { - $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']); + $ts = new \DateTimeImmutable(self::TIMESTAMP); $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z')); } @@ -1613,29 +1749,60 @@ private function noop() public function testDBDatabaseRole() { + $this->spannerClient->createSession( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['session']['creatorRole'], 'Reader'); + return $message['database'] == $this->database->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new SessionProto(['name' => $this->session->name()])); + $sql = $this->createStreamingAPIArgs()['sql']; - $this->connection->createSession(Argument::withEntry( - 'session', - ['labels' => [], 'creator_role' => 'Reader'] - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => $this->session->name() - ]); - $this->connection->executeStreamingSql(Argument::withEntry('sql', $sql)) - ->shouldBeCalled()->willReturn($this->resultGenerator()); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $this->assertEquals($request->getSql(), $sql); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); + + $this->spannerClient->deleteSession( + Argument::type(DeleteSessionRequest::class), + Argument::type('array') + )->shouldBeCalledOnce(); - $this->databaseWithDatabaseRole->execute($sql); + $databaseWithDatabaseRole = new Database( + $this->spannerClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, + $this->instance, + self::PROJECT, + self::DATABASE, + ['databaseRole' => 'Reader'] + ); + $databaseWithDatabaseRole->execute($sql); } public function testExecuteWithDirectedRead() { - $this->connection->executeStreamingSql(Argument::withEntry( - 'directedReadOptions', - $this->directedReadOptionsIncludeReplicas - )) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['directedReadOptions'], + self::DIRECTED_READ_OPTIONS_INCLUDE_REPLICAS + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $sql = 'SELECT * FROM Table'; $res = $this->database->execute($sql); @@ -1646,17 +1813,24 @@ public function testExecuteWithDirectedRead() public function testPrioritizeExecuteDirectedReadOptions() { - $this->connection->executeStreamingSql(Argument::withEntry( - 'directedReadOptions', - $this->directedReadOptionsExcludeReplicas - )) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['directedReadOptions'], + self::DIRECTED_READ_OPTIONS_EXCLUDE_REPLICAS + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $sql = 'SELECT * FROM Table'; $res = $this->database->execute( $sql, - ['directedReadOptions' => $this->directedReadOptionsExcludeReplicas] + ['directedReadOptions' => self::DIRECTED_READ_OPTIONS_EXCLUDE_REPLICAS] ); $this->assertInstanceOf(Result::class, $res); $rows = iterator_to_array($res->rows()); @@ -1668,12 +1842,19 @@ public function testReadWithDirectedRead() $table = 'foo'; $keys = [10, 'bar']; $columns = ['id', 'name']; - $this->connection->streamingRead(Argument::withEntry( - 'directedReadOptions', - $this->directedReadOptionsIncludeReplicas - )) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); + $this->spannerClient->streamingRead( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['directedReadOptions'], + self::DIRECTED_READ_OPTIONS_INCLUDE_REPLICAS + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $res = $this->database->read( $table, @@ -1690,18 +1871,25 @@ public function testPrioritizeReadDirectedReadOptions() $table = 'foo'; $keys = [10, 'bar']; $columns = ['id', 'name']; - $this->connection->streamingRead(Argument::withEntry( - 'directedReadOptions', - $this->directedReadOptionsExcludeReplicas - )) - ->shouldBeCalled() - ->willReturn($this->resultGenerator()); + $this->spannerClient->streamingRead( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['directedReadOptions'], + self::DIRECTED_READ_OPTIONS_EXCLUDE_REPLICAS + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $res = $this->database->read( $table, new KeySet(['keys' => $keys]), $columns, - ['directedReadOptions' => $this->directedReadOptionsExcludeReplicas] + ['directedReadOptions' => self::DIRECTED_READ_OPTIONS_EXCLUDE_REPLICAS] ); $this->assertInstanceOf(Result::class, $res); $rows = iterator_to_array($res->rows()); @@ -1714,7 +1902,6 @@ public function testRunTransactionWithUpdate() $this->stubCommit(); $this->stubExecuteStreamingSql(); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function (Transaction $t) use ($sql) { $t->executeUpdate($sql); @@ -1729,7 +1916,6 @@ public function testRunTransactionWithQuery() $this->stubCommit(); $this->stubExecuteStreamingSql(); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function (Transaction $t) use ($sql) { $t->execute($sql)->rows()->current(); @@ -1745,7 +1931,6 @@ public function testRunTransactionWithRead() $this->stubCommit(); $this->stubStreamingRead(); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function (Transaction $t) use ($keySet, $cols) { $t->read(self::TEST_TABLE_NAME, $keySet, $cols)->rows()->current(); @@ -1760,7 +1945,6 @@ public function testRunTransactionWithUpdateBatch() $this->stubCommit(); $this->stubExecuteBatchDml(); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function (Transaction $t) use ($sql) { $t->executeUpdateBatch([['sql' => $sql]]); @@ -1778,7 +1962,6 @@ public function testRunTransactionWithReadFirst() $this->stubCommit(); $this->stubStreamingRead(); $this->stubExecuteStreamingSql(['id' => self::TRANSACTION]); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function (Transaction $t) use ($keySet, $cols, $sql) { $t->read(self::TEST_TABLE_NAME, $keySet, $cols)->rows()->current(); @@ -1797,7 +1980,6 @@ public function testRunTransactionWithExecuteFirst() $this->stubCommit(); $this->stubStreamingRead(['id' => self::TRANSACTION]); $this->stubExecuteStreamingSql(); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function (Transaction $t) use ($keySet, $cols, $sql) { $t->execute($sql)->rows()->current(); @@ -1817,7 +1999,6 @@ public function testRunTransactionWithUpdateBatchFirst() $this->stubExecuteBatchDml(); $this->stubStreamingRead(['id' => self::TRANSACTION]); $this->stubExecuteStreamingSql(['id' => self::TRANSACTION]); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function (Transaction $t) use ($keySet, $cols, $sql) { $t->executeUpdateBatch([['sql' => $sql]]); @@ -1837,16 +2018,38 @@ public function testRunTransactionWithUpdateBatchError() $this->stubCommit(); $this->stubStreamingRead(['id' => self::TRANSACTION]); $this->stubExecuteStreamingSql(['id' => self::TRANSACTION]); - $this->refreshOperation($this->database, $this->connection->reveal()); - $this->connection->executeBatchDml(Argument::allOf( - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]), - Argument::withEntry('transaction', self::BEGIN_RW_OPTIONS), - ))->shouldBeCalled()->willReturn([ - 'status' => ['code' => Code::INVALID_ARGUMENT], - 'resultSets' => [['metadata' => ['transaction' => ['id' => self::TRANSACTION]]]] - ]); + $this->spannerClient->executeBatchDml( + Argument::that(function (ExecuteBatchDmlRequest $request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['requestOptions']['transactionTag'], + self::TRANSACTION_TAG + ); + $this->assertEquals([ + 'begin' => [ + 'readWrite' => [ + 'readLockMode' => 0, + 'multiplexedSessionPreviousTransactionId' => '', + ], + 'excludeTxnFromChangeStreams' => false, + 'isolationLevel' => 0, + ] + ], $message['transaction']); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new ExecuteBatchDmlResponse([ + 'status' => new Status(['code' => Code::INVALID_ARGUMENT]), + 'result_sets' => [ + new ResultSet([ + 'metadata' => new ResultSetMetadata([ + 'transaction' => new TransactionProto(['id' => self::TRANSACTION]) + ]) + ]) + ] + ])); $this->database->runTransaction(function (Transaction $t) use ($keySet, $cols, $sql) { $result = $t->executeUpdateBatch([['sql' => $sql], ['sql' => $sql]]); @@ -1864,17 +2067,42 @@ public function testRunTransactionWithFirstFailedStatement() $error = new ServerException('RST_STREAM', Code::INTERNAL); // First call with ILB fails - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', self::BEGIN_RW_OPTIONS), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]) - ))->shouldBeCalled()->willThrow($error); - $this->stubCommit(false); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($sql, $message['sql']); + $this->assertEquals( + $message['requestOptions']['transactionTag'], + self::TRANSACTION_TAG + ); + return isset($message['transaction']['begin']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willThrow($error); + // Second call with non ILB return result - $this->stubExecuteStreamingSql(['id' => self::TRANSACTION]); - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($sql, $message['sql']); + $this->assertEquals( + $message['requestOptions']['transactionTag'], + self::TRANSACTION_TAG + ); + return $message['transaction'] == ['id' => self::TRANSACTION]; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream( + null, + new ResultSetStats(['row_count_exact' => 1]), + self::TRANSACTION + )); + + $this->stubCommit(false); $this->database->runTransaction(function ($t) use ($sql) { $t->execute($sql); @@ -1900,43 +2128,34 @@ public function testRunTransactionWithCommitAborted() $this->stubExecuteStreamingSql(); // Second onwards non ILB $this->stubExecuteStreamingSql(['id' => self::TRANSACTION]); - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalledTimes($numOfRetries) - ->willReturn(['id' => self::TRANSACTION]); + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $it = 0; - $this->connection->commit(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) + $commitResponse = $this->commitResponse(); + $this->spannerClient->commit( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['session'] == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalledTimes($numOfRetries + 1) - ->will(function () use (&$it, $abort, $numOfRetries) { + ->will(function () use (&$it, $abort, $numOfRetries, $commitResponse) { $it++; if ($it <= $numOfRetries) { throw $abort; } - return ['commitTimestamp' => TransactionTest::TIMESTAMP]; + return $commitResponse; }); - $this->refreshOperation($this->database, $this->connection->reveal()); - $this->database->runTransaction(function ($t) use ($sql) { $t->execute($sql); $t->commit(); @@ -1949,28 +2168,33 @@ public function testRunTransactionWithBeginTransactionFailure() $error = new ServerException('RST_STREAM', Code::INTERNAL); $sql = $this->createStreamingAPIArgs()['sql']; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', self::BEGIN_RW_OPTIONS), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]) - ))->shouldBeCalled()->willThrow($error); - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ) - )) + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($sql, $message['sql']); + $this->assertEquals( + $message['requestOptions']['transactionTag'], + self::TRANSACTION_TAG + ); + return isset($message['transaction']['begin']); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willThrow($error); + + $this->spannerClient->beginTransaction( + Argument::that(function ($request) use ($sql) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['session'], $this->session->name()); + return true; + }), + Argument::type('array') + ) ->shouldBeCalledTimes(Database::MAX_RETRIES) ->willThrow($error); - $this->connection->commit(Argument::any())->shouldNotBeCalled(); - $this->refreshOperation($this->database, $this->connection->reveal()); + + $this->spannerClient->commit(Argument::cetera())->shouldNotBeCalled(); $this->database->runTransaction(function ($t) use ($sql) { $t->execute($sql); @@ -1981,7 +2205,6 @@ public function testRunTransactionWithBeginTransactionFailure() public function testRunTransactionWithBlindCommit() { $this->stubCommit(false); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function ($t) { $t->insert('Posts', [ @@ -1997,29 +2220,34 @@ public function testRunTransactionWithUnavailableErrorRetry() { $sql = $this->createStreamingAPIArgs()['sql']; $numOfRetries = 2; - $unavailable = new ServiceException('Unavailable', 14); - $result = $this->resultGenerator(true, self::TRANSACTION); + $result = $this->resultGeneratorStream( + null, + new ResultSetStats(['row_count_lower_bound' => 1]), + self::TRANSACTION + ); $it = 0; // First call with ILB results in unavailable error. // Second call also made with ILB, returns ResultSet. - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', self::BEGIN_RW_OPTIONS), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]) - )) + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['sql'], $sql); + $this->assertTrue(isset($message['transaction']['begin'])); + return $message['requestOptions']['transactionTag'] == self::TRANSACTION_TAG; + }), + Argument::type('array') + ) ->shouldBeCalledTimes($numOfRetries) - ->will(function () use (&$it, $unavailable, $numOfRetries, $result) { + ->will(function () use (&$it, $numOfRetries, $result) { $it++; if ($it < $numOfRetries) { - throw $unavailable; + throw new ServiceException('Unavailable', 14); } return $result; }); + $this->stubCommit(); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function ($t) use ($sql) { $t->execute($sql); @@ -2031,22 +2259,29 @@ public function testRunTransactionWithFirstUnavailableErrorRetry() { $sql = $this->createStreamingAPIArgs()['sql']; $unavailable = new ServiceException('Unavailable', 14); + $stream = $this->prophesize(ServerStream::class); + $stream->readAll() + ->willReturn($this->resultGeneratorWithError()); // First call with ILB results in a transaction. // Then the stream fails, Second call needs to use the // transaction created by the first call. - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', self::BEGIN_RW_OPTIONS), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]) - )) - ->shouldBeCalledTimes(1) - ->willreturn($this->resultGeneratorWithError()); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $this->assertEquals($request->getSql(), $sql); + return $this->serializer->decodeMessage( + new TransactionSelector(), + self::BEGIN_RW_OPTIONS + ) == $request->getTransaction() + && $request->getRequestOptions()->getTransactionTag() == self::TRANSACTION_TAG; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($stream->reveal()); + $this->stubExecuteStreamingSql(['id' => self::TRANSACTION]); $this->stubCommit(); - $this->refreshOperation($this->database, $this->connection->reveal()); $this->database->runTransaction(function ($t) use ($sql) { $result = $t->execute($sql); @@ -2074,14 +2309,16 @@ public function testRunTransactionWithUnavailableAndAbortErrorRetry() $it = 0; // First call with ILB results in unavailable error. // Second call also made with ILB, gets aborted. - $this->connection->streamingRead(Argument::allOf( - Argument::withEntry('transaction', self::BEGIN_RW_OPTIONS), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]), - Argument::withEntry('table', self::TEST_TABLE_NAME), - Argument::withEntry('columns', $cols) - )) + $this->spannerClient->streamingRead( + Argument::that(function ($request) use ($cols) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['table'], self::TEST_TABLE_NAME); + $this->assertEquals($message['columns'], $cols); + return isset($message['transaction']['begin']) + && $message['requestOptions']['transactionTag'] == self::TRANSACTION_TAG; + }), + Argument::type('array') + ) ->shouldBeCalledTimes($numOfRetries) ->will(function () use (&$it, $unavailable, $numOfRetries, $abort) { $it++; @@ -2091,34 +2328,32 @@ public function testRunTransactionWithUnavailableAndAbortErrorRetry() throw $abort; } }); + // Should retry with beginTransaction RPC. $this->stubStreamingRead(['id' => self::TRANSACTION]); - $this->connection->beginTransaction(Argument::any()) - ->willReturn(['id' => self::TRANSACTION]) - ->shouldBeCalled(); - $this->connection->commit(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - ]), - Argument::withEntry('transactionId', self::TRANSACTION), - Argument::withEntry('mutations', [['insert' => [ - 'table' => self::TEST_TABLE_NAME, - 'columns' => ['ID', 'title', 'content'], - 'values' => ['10', 'My New Post', 'Hello World'] - ]]]) - )) - ->shouldBeCalledTimes(1) - ->willReturn(['commitTimestamp' => self::TIMESTAMP]); - $this->refreshOperation($this->database, $this->connection->reveal()); + + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); + + $this->spannerClient->commit( + Argument::that(function (CommitRequest $request) { + $this->assertEquals($request->getSession(), $this->session->name()); + $this->assertEquals($request->getTransactionId(), self::TRANSACTION); + $this->assertEquals($request->getRequestOptions()->getTransactionTag(), self::TRANSACTION_TAG); + $this->assertEquals($this->serializer->encodeMessage($request)['mutations'], [['insert' => [ + 'table' => self::TEST_TABLE_NAME, + 'columns' => ['ID', 'title', 'content'], + 'values' => [['10', 'My New Post', 'Hello World']] + ]]]); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); $this->database->runTransaction(function ($t) use ($keySet, $cols) { $t->insert(self::TEST_TABLE_NAME, [ @@ -2136,10 +2371,13 @@ public function testRunTransactionWithRollback() $sql = $this->createStreamingAPIArgs()['sql']; $this->stubExecuteStreamingSql(); - $this->connection->rollback(Argument::allOf( - Argument::withEntry('transactionId', self::TRANSACTION) - ))->shouldBeCalled()->willReturn(null); - $this->refreshOperation($this->database, $this->connection->reveal()); + $this->spannerClient->rollback( + Argument::that(function ($request) use ($sql) { + return $request->getTransactionId() == self::TRANSACTION; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); $this->database->runTransaction(function (Transaction $t) use ($sql) { $t->execute($sql); @@ -2149,36 +2387,26 @@ public function testRunTransactionWithRollback() public function testRunTransactionWithExcludeTxnFromChangeStreams() { - $gapic = $this->prophesize(SpannerClient::class); - - $sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION); - $session = new SessionProto(['name' => $sessName]); - $resultSet = new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])]); - $gapic->createSession(Argument::cetera())->shouldBeCalled()->willReturn($session); - $gapic->deleteSession(Argument::cetera())->shouldBeCalled(); - $sql = 'SELECT example FROM sql_query'; $stream = $this->prophesize(ServerStream::class); - $stream->readAll()->shouldBeCalledOnce()->willReturn([$resultSet]); - $gapic->executeStreamingSql($sessName, $sql, Argument::that(function (array $options) { - $this->assertArrayHasKey('transaction', $options); - $this->assertNotNull($transactionOptions = $options['transaction']->getBegin()); - $this->assertTrue($transactionOptions->getExcludeTxnFromChangeStreams()); - return true; - })) + $stream->readAll() ->shouldBeCalledOnce() - ->willReturn($stream->reveal()); + ->willReturn([ + new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])]) + ]); - $database = new Database( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - $this->instance, - $this->lro->reveal(), - $this->lroCallables, - self::PROJECT, - self::DATABASE - ); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) { + $this->assertNotNull($transactionOptions = $request->getTransaction()->getBegin()); + $this->assertTrue($transactionOptions->getExcludeTxnFromChangeStreams()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($stream->reveal()); - $database->runTransaction( + $this->database->runTransaction( function (Transaction $t) use ($sql) { // Run a fake query $t->executeUpdate($sql); @@ -2188,31 +2416,33 @@ function (Transaction $t) use ($sql) { $prop->setAccessible(true); $prop->setValue($t, Transaction::STATE_COMMITTED); }, - ['transactionOptions' => ['excludeTxnFromChangeStreams' => true]] + [ + 'transactionOptions' => ['excludeTxnFromChangeStreams' => true] + ] ); } public function testExecutePartitionedUpdateWithExcludeTxnFromChangeStreams() { - $gapic = $this->prophesize(SpannerClient::class); - - $sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION); - $session = new SessionProto(['name' => $sessName]); - $gapic->createSession(Argument::cetera())->shouldBeCalled()->willReturn($session); - $gapic->deleteSession(Argument::cetera())->shouldBeCalled(); - $sql = 'SELECT example FROM sql_query'; - $resultSet = new ResultSet(['stats' => new ResultSetStats(['row_count_lower_bound' => 0])]); + $stream = $this->prophesize(ServerStream::class); - $stream->readAll()->shouldBeCalledOnce()->willReturn([$resultSet]); - $gapic->executeStreamingSql($sessName, $sql, Argument::type('array')) + $stream->readAll() + ->shouldBeCalledOnce() + ->willReturn([ + new ResultSet(['stats' => new ResultSetStats(['row_count_lower_bound' => 0])]) + ]); + + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) ->shouldBeCalledOnce() ->willReturn($stream->reveal()); - $gapic->beginTransaction( - $sessName, - Argument::that(function (TransactionOptions $options) { - $this->assertTrue($options->getExcludeTxnFromChangeStreams()); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertTrue($request->getOptions()->getExcludeTxnFromChangeStreams()); return true; }), Argument::type('array') @@ -2220,16 +2450,7 @@ public function testExecutePartitionedUpdateWithExcludeTxnFromChangeStreams() ->shouldBeCalledOnce() ->willReturn(new TransactionProto(['id' => 'foo'])); - $database = new Database( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - $this->instance, - $this->lro->reveal(), - $this->lroCallables, - self::PROJECT, - self::DATABASE - ); - - $database->executePartitionedUpdate( + $this->database->executePartitionedUpdate( $sql, ['transactionOptions' => ['excludeTxnFromChangeStreams' => true]] ); @@ -2237,72 +2458,42 @@ public function testExecutePartitionedUpdateWithExcludeTxnFromChangeStreams() public function testBatchWriteWithExcludeTxnFromChangeStreams() { - $gapic = $this->prophesize(SpannerClient::class); - - $sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION); - $session = new SessionProto(['name' => $sessName]); - $gapic->createSession(Argument::cetera())->shouldBeCalled()->willReturn($session); - $gapic->deleteSession(Argument::cetera())->shouldBeCalled(); - - $mutationGroups = []; - $gapic->batchWrite( - $sessName, - $mutationGroups, - Argument::that(function ($options) { - $this->assertArrayHasKey('excludeTxnFromChangeStreams', $options); - $this->assertTrue($options['excludeTxnFromChangeStreams']); + $this->spannerClient->batchWrite( + Argument::that(function (BatchWriteRequest $request) { + $this->assertTrue($request->getExcludeTxnFromChangeStreams()); return true; - }) + }), + Argument::type('array') ) ->shouldBeCalledOnce() - ->willReturn(new TransactionProto(['id' => 'foo'])); - - $database = new Database( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - $this->instance, - $this->lro->reveal(), - $this->lroCallables, - self::PROJECT, - self::DATABASE - ); + ->willReturn($this->resultGeneratorStream()); - $database->batchWrite($mutationGroups, [ + $this->database->batchWrite([], [ 'excludeTxnFromChangeStreams' => true ]); } public function testRunTransactionIsolationLevel() { - $gapic = $this->prophesize(SpannerClient::class); - - $sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION); - $session = new SessionProto(['name' => $sessName]); - $resultSet = new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])]); - $gapic->createSession(Argument::cetera())->shouldBeCalled()->willReturn($session); - $gapic->deleteSession(Argument::cetera())->shouldBeCalled(); - $sql = 'SELECT example FROM sql_query'; $stream = $this->prophesize(ServerStream::class); - $stream->readAll()->shouldBeCalledOnce()->willReturn([$resultSet]); - $gapic->executeStreamingSql($sessName, $sql, Argument::that(function (array $options) { - $this->assertArrayHasKey('transaction', $options); - $this->assertNotNull($transactionOptions = $options['transaction']->getBegin()); - $this->assertEquals(IsolationLevel::REPEATABLE_READ, $transactionOptions->getIsolationLevel()); - return true; - })) + $stream->readAll() ->shouldBeCalledOnce() - ->willReturn($stream->reveal()); + ->willReturn([new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])])]); - $database = new Database( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - $this->instance, - $this->lro->reveal(), - $this->lroCallables, - self::PROJECT, - self::DATABASE - ); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) { + $txnOptions = $request->getTransaction()->getBegin(); + $this->assertNotNull($txnOptions); + $this->assertEquals(IsolationLevel::REPEATABLE_READ, $txnOptions->getIsolationLevel()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($stream->reveal()); - $database->runTransaction( + $this->database->runTransaction( function (Transaction $t) use ($sql) { // Run a fake query $t->executeUpdate($sql); @@ -2318,50 +2509,29 @@ function (Transaction $t) use ($sql) { public function testRunTransactionWithReadLockMode() { - $expectedReadLockMode = ReadLockMode::OPTIMISTIC; - - $gapic = $this->prophesize(SpannerClient::class); - - $sessName = SpannerClient::sessionName(self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION); - $session = new SessionProto(['name' => $sessName]); - $resultSet = new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])]); - $gapic->createSession(Argument::cetera())->shouldBeCalled()->willReturn($session); - $gapic->deleteSession(Argument::cetera())->shouldBeCalled(); - $sql = 'SELECT example FROM sql_query'; $stream = $this->prophesize(ServerStream::class); - $stream->readAll()->shouldBeCalledOnce()->willReturn([$resultSet]); - $gapic->executeStreamingSql( - $sessName, - $sql, - Argument::that(function (array $options) use ($expectedReadLockMode) { - $this->assertArrayHasKey('transaction', $options); - $this->assertNotNull($transactionOptions = $options['transaction']->getBegin()); - $this->assertNotNull($readWriteTxnOptions = $transactionOptions->getReadWrite()); - $this->assertNotNull($readLockModeOption = $readWriteTxnOptions->getReadLockMode()); - $this->assertEquals( - $expectedReadLockMode, - $readLockModeOption - ); + $stream->readAll() + ->shouldBeCalledOnce() + ->willReturn([new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])])]); + + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) { + $txnOptions = $request->getTransaction()->getBegin(); + $this->assertNotNull($txnOptions); + $this->assertNotNull($readWriteTxnOptions = $txnOptions->getReadWrite()); + $this->assertEquals(ReadLockMode::OPTIMISTIC, $readWriteTxnOptions->getReadLockMode()); return true; - }) + }), + Argument::type('array') ) ->shouldBeCalledOnce() ->willReturn($stream->reveal()); - $database = new Database( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - $this->instance, - $this->lro->reveal(), - $this->lroCallables, - self::PROJECT, - self::DATABASE - ); - // Test TransactionOption array format with base level property set for readLockMode // This helps test proper formating by the library to the format expected by Spanner backend // (i.e. readLockMode should be inside readWrite) - $database->runTransaction( + $this->database->runTransaction( function (Transaction $t) use ($sql) { // Run a fake query $t->executeUpdate($sql); @@ -2371,32 +2541,27 @@ function (Transaction $t) use ($sql) { $prop->setAccessible(true); $prop->setValue($t, Transaction::STATE_COMMITTED); }, - ['transactionOptions' => ['readLockMode' => $expectedReadLockMode, ] ] + ['transactionOptions' => ['readLockMode' => ReadLockMode::OPTIMISTIC]] ); } public function testTransactionWithReadLockMode() { - $expectedReadLockMode = ReadLockMode::OPTIMISTIC; - - $this->connection->beginTransaction( - Argument::that(function (array $args) use ($expectedReadLockMode) { - $this->assertArrayHasKey('transactionOptions', $args); - $this->assertArrayHasKey('readWrite', $args['transactionOptions']); - $this->assertArrayHasKey('readLockMode', $args['transactionOptions']['readWrite']); - $this->assertEquals( - $expectedReadLockMode, - $args['transactionOptions']['readWrite']['readLockMode'], - "The read lock mode received was {$args['transactionOptions']['readWrite']['readLockMode']} " . - "does not match expected {$expectedReadLockMode}" - ); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertNotNull($txnOptions = $request->getOptions()); + $this->assertNotNull($readWrite = $txnOptions->getReadWrite()); + $this->assertEquals(ReadLockMode::OPTIMISTIC, $readWrite->getReadLockMode()); return true; - }) + }), + Argument::type('array') ) ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $t = $this->database->transaction(['transactionOptions' => ['readLockMode' => $expectedReadLockMode, ]]); + $t = $this->database->transaction([ + 'transactionOptions' => ['readLockMode' => ReadLockMode::OPTIMISTIC] + ]); $this->assertInstanceOf(Transaction::class, $t); } @@ -2414,22 +2579,20 @@ private function createStreamingAPIArgs() private function resultGeneratorWithError() { - $fields = [ + $fields = new Field([ 'name' => 'ID', - 'value' => ['code' => Database::TYPE_INT64] - ]; - $values = [10]; - $result = [ - 'metadata' => [ - 'rowType' => [ - 'fields' => $fields - ] - ], + 'type' => new TypeProto(['code' => Database::TYPE_INT64]) + ]); + $values = [new Value(['number_value' => 10])]; + $result = new PartialResultSet([ + 'metadata' => new ResultSetMetadata([ + 'row_type' => new StructType([ + 'fields' => [$fields] + ]), + 'transaction' => new TransactionProto(['id' => self::TRANSACTION]) + ]), 'values' => $values - ]; - $result['metadata']['transaction'] = [ - 'id' => self::TRANSACTION - ]; + ]); yield $result; throw new ServiceException('Unavailable', 14); @@ -2438,67 +2601,97 @@ private function resultGeneratorWithError() private function stubCommit($withTransaction = true) { if ($withTransaction) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotBeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); } else { - $this->connection->beginTransaction(Argument::any()) - ->willReturn(['id' => self::TRANSACTION]) - ->shouldBeCalled(); + $this->spannerClient->beginTransaction( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); } - $this->connection->commit(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry( - 'database', - DatabaseAdminClient::databaseName( - self::PROJECT, - self::INSTANCE, - self::DATABASE - ) - ), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - ]), - Argument::withEntry('transactionId', self::TRANSACTION) - )) - ->shouldBeCalled() - ->willReturn(['commitTimestamp' => self::TIMESTAMP]); + + $this->spannerClient->commit( + Argument::that(function (CommitRequest $request) { + $this->assertEquals($request->getSession(), $this->session->name()); + $this->assertEquals($request->getTransactionId(), self::TRANSACTION); + $this->assertEquals($request->getRequestOptions()->getTransactionTag(), self::TRANSACTION_TAG); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); } private function stubStreamingRead($transactionOptions = self::BEGIN_RW_OPTIONS) { - $keySet = $this->createStreamingAPIArgs()['keySet']; $cols = $this->createStreamingAPIArgs()['cols']; - $this->connection->streamingRead(Argument::allOf( - Argument::withEntry('transaction', $transactionOptions), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]), - Argument::withEntry('table', self::TEST_TABLE_NAME), - Argument::withEntry('columns', $cols) - ))->shouldBeCalled()->willReturn($this->resultGenerator(true, self::TRANSACTION)); + $this->spannerClient->streamingRead( + Argument::that(function (ReadRequest $request) use ($transactionOptions, $cols) { + return $request->getTransaction() == $this->serializer->decodeMessage( + new TransactionSelector(), + $transactionOptions + ) + && $request->getTable() == self::TEST_TABLE_NAME + && iterator_to_array($request->getColumns()) == $cols + && $request->getRequestOptions()->getTransactionTag() == self::TRANSACTION_TAG; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream( + null, + new ResultSetStats(['row_count_exact' => 1]), + self::TRANSACTION + )); } private function stubExecuteStreamingSql($transactionOptions = self::BEGIN_RW_OPTIONS) { $sql = $this->createStreamingAPIArgs()['sql']; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', $transactionOptions), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator(true, self::TRANSACTION)); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql, $transactionOptions) { + return $request->getSql() == $sql + && $request->getTransaction() == $this->serializer->decodeMessage( + new TransactionSelector(), + $transactionOptions + ) + && $request->getRequestOptions()->getTransactionTag() == self::TRANSACTION_TAG; + }), + Argument::type('array') + ) + ->shouldBeCalled() + ->willReturn($this->resultGeneratorStream( + null, + new ResultSetStats(['row_count_exact' => 1]), + self::TRANSACTION + )); } private function stubExecuteBatchDml($transactionOptions = self::BEGIN_RW_OPTIONS) { - $this->connection->executeBatchDml(Argument::allOf( - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG - ]), - Argument::withEntry('transaction', $transactionOptions), - ))->shouldBeCalled()->willReturn([ - 'resultSets' => [['metadata' => ['transaction' => ['id' => self::TRANSACTION]]]] - ]); + $this->spannerClient->executeBatchDml( + Argument::that(function (ExecuteBatchDmlRequest $request) use ($transactionOptions) { + $this->assertEquals( + $request->getTransaction(), + $this->serializer->decodeMessage(new TransactionSelector(), $transactionOptions) + ); + $this->assertEquals( + $request->getRequestOptions()->getTransactionTag(), + self::TRANSACTION_TAG + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new ExecuteBatchDmlResponse([ + 'result_sets' => [ + new ResultSet(['metadata' => new ResultSetMetadata([ + 'transaction' => new TransactionProto(['id' => self::TRANSACTION]) + ])]) + ] + ])); } } diff --git a/Spanner/tests/Unit/DateTest.php b/Spanner/tests/Unit/DateTest.php index 1e16f5c8360a..ee4a3d9a32cf 100644 --- a/Spanner/tests/Unit/DateTest.php +++ b/Spanner/tests/Unit/DateTest.php @@ -17,8 +17,8 @@ namespace Google\Cloud\Spanner\Tests\Unit; -use Google\Cloud\Spanner\Date; use Google\Cloud\Core\Testing\GrpcTestTrait; +use Google\Cloud\Spanner\Date; use PHPUnit\Framework\TestCase; /** @@ -57,7 +57,7 @@ public function testFormatAsString() public function testCast() { - $this->assertEquals($this->dt->format(Date::FORMAT), (string)$this->date); + $this->assertEquals($this->dt->format(Date::FORMAT), (string) $this->date); } public function testType() diff --git a/Spanner/tests/Unit/DurationTest.php b/Spanner/tests/Unit/DurationTest.php deleted file mode 100644 index 22eb0495f947..000000000000 --- a/Spanner/tests/Unit/DurationTest.php +++ /dev/null @@ -1,66 +0,0 @@ -<?php -/** - * Copyright 2016 Google Inc. - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit; - -use Google\Cloud\Spanner\Duration; -use PHPUnit\Framework\TestCase; - -/** - * @group spanner - */ -class DurationTest extends TestCase -{ - const SECONDS = 10; - const NANOS = 1; - - private $duration; - - public function setUp(): void - { - $this->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/Spanner/tests/Unit/Fixtures.php b/Spanner/tests/Unit/Fixtures.php index 44a9d28eb2bb..20b74b87b727 100644 --- a/Spanner/tests/Unit/Fixtures.php +++ b/Spanner/tests/Unit/Fixtures.php @@ -25,11 +25,6 @@ public static function STREAMING_READ_ACCEPTANCE_FIXTURE() return __DIR__ . '/fixtures/streaming-read-acceptance-test.json'; } - public static function INSTANCE_FIXTURE() - { - return __DIR__ . '/fixtures/instance.json'; - } - public static function INSTANCE_CONFIG_FIXTURE() { return __DIR__ . '/fixtures/instanceConfig.json'; diff --git a/Spanner/tests/Unit/GapicBackoff/GapicBackoffTest.php b/Spanner/tests/Unit/GapicBackoff/GapicBackoffTest.php deleted file mode 100644 index 1d22aa87a908..000000000000 --- a/Spanner/tests/Unit/GapicBackoff/GapicBackoffTest.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php -/** - * Copyright 2020 Google Inc. - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit\GapicBackoff; - -use PHPUnit\Framework\TestCase; -use Google\Cloud\Spanner\SpannerClient; -use Google\Cloud\Core\Testing\GrpcTestTrait; - -/** - * @group spanner - * @group spanner-gapic-backoff - */ -class GapicBackoffTest extends TestCase -{ - use GrpcTestTrait; - - public function setUp(): void - { - $this->checkAndSkipGrpcTests(); - } - - /** - * @param array $config - * @return \Google\Cloud\Core\GrpcRequestWrapper - */ - private function getWrapper($config) - { - $config += [ - 'apiEndpoint' => '127.0.0.1:10', - 'hasEmulator' => true, - ]; - - $spanner = new SpannerClient($config); - $connection = $spanner->instance('nonexistent')->database('nonexistent')->connection(); - return $connection->requestWrapper(); - } - - public function provideDisabledBackoffConfigs() - { - return [ - [[]], - [['useDiscreteBackoffs' => false]], - ]; - } - - /** - * @dataProvider provideDisabledBackoffConfigs - * @param array $config - */ - public function testBackoffDisabledByDefault($config) - { - $wrapper = $this->getWrapper($config); - $handler = function () { - $args = func_get_args(); - return new MockOperationResponse('test', null, array_pop($args)); - }; - $response = $wrapper->send($handler, [[]]); - $expected = [ - 'retriesEnabled' => false, - ]; - $this->assertEquals($expected, $response->options['retrySettings']); - } - - public function testBackoffEnabledManually() - { - $config = [ - 'useDiscreteBackoffs' => true, - ]; - $wrapper = $this->getWrapper($config); - $handler = function () { - $args = func_get_args(); - return new MockOperationResponse('test', null, array_pop($args)); - }; - $response = $wrapper->send($handler, [[]]); - $expected = []; - $this->assertEquals($expected, $response->options['retrySettings']); - } - - public function testUserConfigsAreNotRuined() - { - $retrySettings = [ - 'retriesEnabled' => false, - 'noRetriesRpcTimeoutMillis' => 1234, - ]; - $config = [ - 'useDiscreteBackoffs' => true, - 'grpcOptions' => [ - 'retrySettings' => $retrySettings, - ], - ]; - $wrapper = $this->getWrapper($config); - $handler = function () { - $args = func_get_args(); - return new MockOperationResponse('test', null, array_pop($args)); - }; - $response = $wrapper->send($handler, [[]]); - $this->assertEquals($retrySettings, $response->options['retrySettings']); - } -} diff --git a/Spanner/tests/Unit/GapicBackoff/MockOperationResponse.php b/Spanner/tests/Unit/GapicBackoff/MockOperationResponse.php deleted file mode 100644 index 156428c89863..000000000000 --- a/Spanner/tests/Unit/GapicBackoff/MockOperationResponse.php +++ /dev/null @@ -1,31 +0,0 @@ -<?php -/** - * Copyright 2020 Google Inc. - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit\GapicBackoff; - -use \Google\ApiCore\OperationResponse; - -class MockOperationResponse extends OperationResponse -{ - public $options; - - public function __construct($operationName, $operationsClient, $options = []) - { - $this->options = $options; - parent::__construct($operationName, $operationsClient, $options); - } -} diff --git a/Spanner/tests/Unit/InstanceConfigurationTest.php b/Spanner/tests/Unit/InstanceConfigurationTest.php index 944e8bf99291..93c3ece1a348 100644 --- a/Spanner/tests/Unit/InstanceConfigurationTest.php +++ b/Spanner/tests/Unit/InstanceConfigurationTest.php @@ -17,13 +17,21 @@ namespace Google\Cloud\Spanner\Tests\Unit; -use Google\Cloud\Core\Exception\NotFoundException; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\ApiCore\ApiException; +use Google\ApiCore\OperationResponse; use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\DeleteInstanceConfigRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\GetInstanceConfigRequest; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; +use Google\Cloud\Spanner\Admin\Instance\V1\UpdateInstanceConfigMetadata; +use Google\Cloud\Spanner\Admin\Instance\V1\UpdateInstanceConfigRequest; use Google\Cloud\Spanner\InstanceConfiguration; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Serializer; +use Google\LongRunning\Client\OperationsClient; +use Google\LongRunning\Operation; +use Google\Protobuf\Any; +use Google\Rpc\Code; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -36,183 +44,242 @@ class InstanceConfigurationTest extends TestCase { use GrpcTestTrait; use ProphecyTrait; - use StubCreationTrait; const PROJECT_ID = 'test-project'; const NAME = 'test-config'; - private $connection; - private $configuration; + private $instanceAdminClient; + private $operationsClient; + private Serializer $serializer; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); - $this->configuration = TestHelpers::stub(InstanceConfiguration::class, [ - $this->connection->reveal(), - self::PROJECT_ID, - self::NAME, - [], - $this->prophesize(LongRunningConnectionInterface::class)->reveal() - ]); + $this->operationsClient = $this->prophesize(OperationsClient::class); + $this->instanceAdminClient = $this->prophesize(InstanceAdminClient::class); + $this->instanceAdminClient->getOperationsClient() + ->willReturn($this->operationsClient->reveal()); + $this->serializer = new Serializer(); } public function testName() { + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, + self::PROJECT_ID, + self::NAME + ); + $this->assertEquals( - InstanceAdminClient::parseName($this->configuration->name())['instance_config'], + InstanceAdminClient::parseName($instanceConfig->name())['instance_config'], self::NAME ); } public function testInfo() { - $this->connection->getInstanceConfig(Argument::any())->shouldNotBeCalled(); - $this->configuration->___setProperty('connection', $this->connection->reveal()); - $info = ['foo' => 'bar']; - $config = TestHelpers::stub(InstanceConfiguration::class, [ - $this->connection->reveal(), + $this->instanceAdminClient->getInstanceConfig(Argument::cetera()) + ->shouldNotBeCalled(); + + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, self::PROJECT_ID, self::NAME, - $info, - $this->prophesize(LongRunningConnectionInterface::class)->reveal() - ]); + ['instanceConfig' => $info], + ); - $this->assertEquals($info, $config->info()); + $this->assertEquals($info, $instanceConfig->info()); } public function testInfoWithReload() { - $info = ['foo' => 'bar']; - - $this->connection->getInstanceConfig([ - 'name' => InstanceAdminClient::instanceConfigName(self::PROJECT_ID, self::NAME), - 'projectName' => InstanceAdminClient::projectName(self::PROJECT_ID) - ])->shouldBeCalled()->willReturn($info); - - $this->configuration->___setProperty('connection', $this->connection->reveal()); + $expected = ['display_name' => 'foo']; + $this->instanceAdminClient->getInstanceConfig( + Argument::type(GetInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceConfig($expected)); + + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, + self::PROJECT_ID, + self::NAME + ); + $info = $instanceConfig->info(); - $this->assertEquals($info, $this->configuration->info()); + $this->assertArrayHasKey('displayName', $info); + $this->assertEquals($expected['display_name'], $info['displayName']); } public function testExists() { - $this->connection->getInstanceConfig(Argument::allOf( - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::withEntry('name', $this->configuration->name()) - )) - ->shouldBeCalled() - ->willReturn([]); - $this->configuration->___setProperty('connection', $this->connection->reveal()); - - $this->assertTrue($this->configuration->exists()); + $this->instanceAdminClient->getInstanceConfig( + Argument::type(GetInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceConfig()); + + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, + self::PROJECT_ID, + self::NAME + ); + $this->assertTrue($instanceConfig->exists()); } public function testExistsDoesntExist() { - $this->connection->getInstanceConfig(Argument::allOf( - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::withEntry('name', $this->configuration->name()) - )) - ->shouldBeCalled() - ->willThrow(new NotFoundException('', 404)); - $this->configuration->___setProperty('connection', $this->connection->reveal()); - - $this->assertFalse($this->configuration->exists()); + $this->instanceAdminClient->getInstanceConfig( + Argument::type(GetInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->will(function () { + throw new ApiException('', Code::NOT_FOUND); + }); + + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, + self::PROJECT_ID, + self::NAME + ); + + $this->assertFalse($instanceConfig->exists()); } public function testReload() { - $info = ['foo' => 'bar']; - - $this->connection->getInstanceConfig([ - 'name' => InstanceAdminClient::instanceConfigName(self::PROJECT_ID, self::NAME), - 'projectName' => InstanceAdminClient::projectName(self::PROJECT_ID) - ])->shouldBeCalledTimes(1)->willReturn($info); - - $this->configuration->___setProperty('connection', $this->connection->reveal()); - - $info = $this->configuration->reload(); + $expected1 = ['some' => 'info']; + $expected2 = ['display_name' => 'bar']; + $this->instanceAdminClient->getInstanceConfig( + Argument::type(GetInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceConfig($expected2)); + + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, + self::PROJECT_ID, + self::NAME, + ['instanceConfig' => $expected1], + ); - $info2 = $this->configuration->info(); + $info1 = $instanceConfig->info(); + $info2 = $instanceConfig->reload(); + $info3 = $instanceConfig->info(); - $this->assertEquals($info, $info2); + $this->assertEquals($expected1, $info1); + $this->assertNotEquals($info1, $info2); + $this->assertArrayHasKey('displayName', $info2); + $this->assertEquals($expected2['display_name'], $info2['displayName']); + $this->assertEquals($info2, $info3); } public function testUpdate() { - $config = $this->getDefaultInstance(); - - $this->connection->updateInstanceConfig([ - 'name' => $config['name'], - 'displayName' => 'bar', - ])->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' + $expectedInstanceConfig = new InstanceConfig([ + 'name' => InstanceAdminClient::instanceConfigName(self::PROJECT_ID, 'foo'), + 'display_name' => 'bar2' + ]); + $result = new Any(); + $result->pack($expectedInstanceConfig); + $metadata = new Any(); + $metadata->pack(new UpdateInstanceConfigMetadata()); + $operationProto = new Operation([ + 'response' => $result, + 'metadata' => $metadata, + 'done' => true ]); - $this->configuration->___setProperty('connection', $this->connection->reveal()); - - $this->configuration->update(['displayName' => 'bar']); - } - - public function testUpdateWithExistingLabels() - { - $config = $this->getDefaultInstance(); - $config['labels'] = ['foo' => 'bar']; - - $this->connection->updateInstanceConfig([ - 'labels' => $config['labels'], - 'name' => $config['name'], - ])->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' + $operationResponse = new OperationResponse('operation-name', $this->operationsClient->reveal(), [ + 'operationReturnType' => InstanceConfig::class, + 'lastProtoResponse' => $operationProto, ]); + $this->instanceAdminClient->resumeOperation($operationResponse->getName()) + ->shouldBeCalledOnce() + ->willReturn($operationResponse); + + $this->instanceAdminClient->updateInstanceConfig( + Argument::that(function (UpdateInstanceConfigRequest $request) use ($expectedInstanceConfig) { + $instanceConfig = $request->getInstanceConfig(); + return $instanceConfig->getDisplayName() === $expectedInstanceConfig->getDisplayName() + && $instanceConfig->getName() === $expectedInstanceConfig->getName(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($operationResponse); + + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, + self::PROJECT_ID, + 'foo', + ); - $this->configuration->___setProperty('connection', $this->connection->reveal()); + $operation = $instanceConfig->update(['displayName' => 'bar2']); + $operation->pollUntilComplete(); + $updatedInstanceConfig = $operation->result(); - $this->configuration->update(['labels' => $config['labels']]); + $info = $updatedInstanceConfig->info(); + $this->assertEquals('bar2', $info['displayName']); } public function testUpdateWithChanges() { - $config = $this->getDefaultInstance(); - - $changes = [ - 'labels' => [ - 'foo' => 'bar' - ], + $config = [ + 'name' => InstanceAdminClient::instanceConfigName(self::PROJECT_ID, self::NAME), + 'labels' => ['foo' => 'bar'], 'displayName' => 'New Name', ]; - $this->connection->updateInstanceConfig([ - 'name' => $config['name'], - 'displayName' => $changes['displayName'], - 'labels' => $changes['labels'], - ])->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' - ]); - - $this->configuration->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->updateInstanceConfig( + Argument::that(function (UpdateInstanceConfigRequest $request) use ($config) { + $instanceConfig = $request->getInstanceConfig()->serializeToJsonString(); + return json_decode($instanceConfig, true) == $config; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->prophesize(OperationResponse::class)->reveal()); + + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, + self::PROJECT_ID, + self::NAME + ); - $this->configuration->update($changes); + $instanceConfig->update(['displayName' => 'New Name', 'labels' => ['foo' => 'bar']]); } public function testDelete() { - $this->connection->deleteInstanceConfig([ - 'name' => InstanceAdminClient::instanceConfigName(self::PROJECT_ID, self::NAME) - ])->shouldBeCalled(); - - $this->configuration->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->deleteInstanceConfig( + Argument::type(DeleteInstanceConfigRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce(); + + $instanceConfig = new InstanceConfiguration( + $this->instanceAdminClient->reveal(), + $this->serializer, + self::PROJECT_ID, + self::NAME + ); - $this->configuration->delete(); + $instanceConfig->delete(); } private function getDefaultInstance() diff --git a/Spanner/tests/Unit/InstanceTest.php b/Spanner/tests/Unit/InstanceTest.php index cb0564d92e0e..e85f7fce7a9d 100644 --- a/Spanner/tests/Unit/InstanceTest.php +++ b/Spanner/tests/Unit/InstanceTest.php @@ -17,26 +17,42 @@ namespace Google\Cloud\Spanner\Tests\Unit; +use Google\ApiCore\OperationResponse; +use Google\ApiCore\Page; +use Google\ApiCore\PagedListResponse; use Google\Cloud\Core\Exception\NotFoundException; -use Google\Cloud\Core\Iam\Iam; +use Google\Cloud\Core\Iam\IamManager; use Google\Cloud\Core\Iterator\ItemIterator; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Backup as BackupProto; +use Google\Cloud\Spanner\Admin\Database\V1\Backup\State; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Database\V1\Database as DatabaseProto; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupOperationsResponse; +use Google\Cloud\Spanner\Admin\Database\V1\ListBackupsResponse; +use Google\Cloud\Spanner\Admin\Database\V1\ListDatabaseOperationsResponse; +use Google\Cloud\Spanner\Admin\Database\V1\ListDatabasesResponse; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Instance as InstanceProto; +use Google\Cloud\Spanner\Backup; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; -use Google\Cloud\Spanner\Tests\StubCreationTrait; -use Google\Cloud\Spanner\Backup; +use Google\Cloud\Spanner\KeySet; +use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\DeleteSessionRequest; +use Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\Session; +use Google\LongRunning\Operation; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Spanner\Result; -use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; /** * @group spanner @@ -47,8 +63,6 @@ class InstanceTest extends TestCase use GrpcTestTrait; use ProphecyTrait; use ResultGeneratorTrait; - use StubCreationTrait; - use ResultGeneratorTrait; const PROJECT_ID = 'test-project'; const NAME = 'instance-name'; @@ -56,38 +70,46 @@ class InstanceTest extends TestCase const BACKUP = 'my-backup'; const SESSION = 'projects/test-project/instances/instance-name/databases/database-name/sessions/session'; - private $connection; - private $instance; - private $lroConnection; private $directedReadOptionsIncludeReplicas; + private $instance; + private $serializer; + private $spannerClient; + private $instanceAdminClient; + private $databaseAdminClient; + private $operationResponse; + private $page; + private $pagedListResponse; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); + $this->serializer = new Serializer(); $this->directedReadOptionsIncludeReplicas = [ 'includeReplicas' => [ - 'replicaSelections' => [ + 'replicaSelections' => [[ 'location' => 'us-central1', - 'type' => 'READ_WRITE', - 'autoFailoverDisabled' => false - ] + 'type' => Type::READ_WRITE, + ]], 'autoFailoverDisabled' => false ] ]; - $this->instance = TestHelpers::stub(Instance::class, [ - $this->connection->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->instanceAdminClient = $this->prophesize(InstanceAdminClient::class); + $this->databaseAdminClient = $this->prophesize(DatabaseAdminClient::class); + $this->operationResponse = $this->prophesize(OperationResponse::class); + $this->page = $this->prophesize(Page::class); + $this->pagedListResponse = $this->prophesize(PagedListResponse::class); + $this->pagedListResponse->getPage()->willReturn($this->page->reveal()); + + $this->instance = new Instance( + $this->spannerClient->reveal(), + $this->instanceAdminClient->reveal(), + $this->databaseAdminClient->reveal(), + $this->serializer, self::PROJECT_ID, self::NAME, - false, - [], ['directedReadOptions' => $this->directedReadOptionsIncludeReplicas] - ], [ - 'info', - 'connection' - ]); + ); } public function testName() @@ -97,50 +119,39 @@ public function testName() public function testInfo() { - $this->connection->getInstance(Argument::any())->shouldNotBeCalled(); - - $this->instance->___setProperty('info', ['foo' => 'bar']); - $this->assertEquals('bar', $this->instance->info()['foo']); - } - - public function testInfoWithReload() - { - $instance = $this->getDefaultInstance(); - - $this->connection->getInstance(Argument::allOf( - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::withEntry('name', $this->instance->name()) - )) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + return $message['name'] == $this->instance->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->getDefaultInstance()); $info = $this->instance->info(); $this->assertEquals('Instance Name', $info['displayName']); + // test calling info again does not reload $this->assertEquals($info, $this->instance->info()); } public function testInfoWithReloadAndFieldMask() { - $instance = [ - 'name' => $this->instance->name(), - 'node_count' => 1 - ]; - - $requestedFieldNames = ["name", 'node_count']; - $this->connection->getInstance(Argument::allOf( - Argument::withEntry('name', $this->instance->name()), - Argument::withEntry('fieldMask', $requestedFieldNames) - )) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $requestedFieldNames = ['name', 'node_count']; + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) use ($requestedFieldNames) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals(['paths' => $requestedFieldNames], $message['fieldMask']); + return $message['name'] == $this->instance->name(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceProto([ + 'name' => $this->instance->name(), + 'node_count' => 1 + ])); $info = $this->instance->info(['fieldMask' => $requestedFieldNames]); @@ -149,32 +160,30 @@ public function testInfoWithReloadAndFieldMask() public function testExists() { - $this->connection->getInstance(Argument::allOf( - Argument::withEntry('name', $this->instance->name()), - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::withEntry('fieldMask', ['name']) - )) - ->shouldBeCalledTimes(1) - ->willReturn([]); - - $this->connection->getInstance(Argument::allOf( - Argument::withEntry('name', $this->instance->name()), - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::not(Argument::withKey('fieldMask')) - )) + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['name'], $this->instance->name()); + return isset($message['fieldMask']) && ['paths' => ['name']] == $message['fieldMask']; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceProto()); + + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['name'], $this->instance->name()); + return !isset($message['fieldMask']); + }), + Argument::type('array') + ) ->shouldBeCalledTimes(2) - ->willReturn([ + ->willReturn(new InstanceProto([ 'name' => $this->instance->name(), - 'nodeCount' => 1, - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + 'node_count' => 1, + ])); $this->assertTrue($this->instance->exists()); @@ -185,36 +194,32 @@ public function testExists() public function testExistsNotFound() { - $this->connection->getInstance(Argument::allOf( - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::withEntry('name', $this->instance->name()) - )) - ->shouldBeCalled() + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['name'], $this->instance->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() ->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::allOf( - Argument::withEntry('name', $this->instance->name()), - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ) - )) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['name'], $this->instance->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->getDefaultInstance()); $info = $this->instance->reload(); @@ -223,24 +228,20 @@ public function testReload() public function testReloadWithFieldMask() { - $instance = [ - 'name' => $this->instance->name(), - 'node_count' => 1 - ]; - - $requestedFieldNames = ["name", 'node_count']; - $this->connection->getInstance(Argument::allOf( - Argument::withEntry('name', $this->instance->name()), - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::withEntry('fieldMask', $requestedFieldNames) - )) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $requestedFieldNames = ['name', 'node_count']; + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) use ($requestedFieldNames) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['name'], $this->instance->name()); + return $message['fieldMask'] == ['paths' => $requestedFieldNames]; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceProto([ + 'name' => $this->instance->name(), + 'node_count' => 1 + ])); $info = $this->instance->reload(['fieldMask' => $requestedFieldNames]); @@ -249,68 +250,65 @@ public function testReloadWithFieldMask() public function testState() { - $instance = $this->getDefaultInstance(); - - $this->connection->getInstance(Argument::allOf( - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::withEntry('name', $this->instance->name()) - )) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['name'], $this->instance->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->getDefaultInstance()); $this->assertEquals(Instance::STATE_READY, $this->instance->state()); } - public function testStateIsNull() + public function testStateIsUnspecified() { - $this->connection->getInstance(Argument::allOf( - Argument::withEntry( - 'projectName', - InstanceAdminClient::projectName(self::PROJECT_ID) - ), - Argument::withEntry('name', $this->instance->name()) - )) - ->shouldBeCalledTimes(1) - ->willReturn([]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); - - $this->assertNull($this->instance->state()); + $this->instanceAdminClient->getInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['name'], $this->instance->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new InstanceProto()); + + $this->assertEquals(State::STATE_UNSPECIFIED, $this->instance->state()); } public function testUpdate() { - $instance = $this->getDefaultInstance(); - - $this->connection->updateInstance([ - 'displayName' => 'bar', - 'name' => $instance['name'], - ])->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->updateInstance( + Argument::that(function ($request) { + $this->assertEquals('bar', $request->getInstance()->getDisplayName()); + $this->assertEquals($this->instance->name(), $request->getInstance()->getName()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $this->instance->update(['displayName' => 'bar']); } public function testUpdateWithProcessingUnits() { - $instance = $this->getDefaultInstance(); - - $this->connection->updateInstance([ - 'processingUnits' => 500, - 'name' => $instance['name'], - ])->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->updateInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals(500, $request->getInstance()->getProcessingUnits()); + $this->assertEquals($this->instance->name(), $request->getInstance()->getName()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $this->instance->update(['processingUnits' => 500]); } @@ -324,54 +322,55 @@ public function testUpdateRaisesInvalidArgument() public function testUpdateWithExistingLabels() { - $instance = $this->getDefaultInstance(); - $instance['labels'] = ['foo' => 'bar']; - - $this->connection->updateInstance([ - 'labels' => $instance['labels'], - 'name' => $instance['name'], - ])->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' - ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->updateInstance( + Argument::that(function ($request) { + $this->assertEquals(['foo' => 'bar'], iterator_to_array($request->getInstance()->getLabels())); + $this->assertEquals($this->instance->name(), $request->getInstance()->getName()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); - $this->instance->update(['labels' => $instance['labels']]); + $this->instance->update(['labels' => ['foo' => 'bar']]); } public function testUpdateWithChanges() { - $instance = $this->getDefaultInstance(); - - $changes = [ + $this->instanceAdminClient->updateInstance( + Argument::that(function ($request) { + $this->assertEquals('New Name', $request->getInstance()->getDisplayName()); + $this->assertEquals(['foo' => 'bar'], iterator_to_array($request->getInstance()->getLabels())); + $this->assertEquals(900, $request->getInstance()->getNodeCount()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); + + $this->instance->update([ 'labels' => [ 'foo' => 'bar' ], 'nodeCount' => 900, 'displayName' => 'New Name', - ]; - - $this->connection->updateInstance([ - 'name' => $instance['name'], - 'displayName' => $changes['displayName'], - 'nodeCount' => $changes['nodeCount'], - 'labels' => $changes['labels'], - ])->shouldBeCalled()->willReturn([ - 'name' => 'my-operation' ]); - - $this->instance->___setProperty('connection', $this->connection->reveal()); - - $this->instance->update($changes); } public function testDelete() { - $this->connection->deleteInstance([ - 'name' => InstanceAdminClient::instanceName(self::PROJECT_ID, self::NAME) - ])->shouldBeCalled(); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->instanceAdminClient->deleteInstance( + Argument::that(function ($request) { + $this->assertEquals( + $request->getName(), + InstanceAdminClient::instanceName(self::PROJECT_ID, self::NAME) + ); + return true; + }), + Argument::type('array') + )->shouldBeCalledOnce(); $this->instance->delete(); } @@ -380,15 +379,22 @@ public function testCreateDatabase() { $extra = ['foo', 'bar']; - $this->connection->createDatabase([ - 'instance' => InstanceAdminClient::instanceName(self::PROJECT_ID, self::NAME), - 'createStatement' => 'CREATE DATABASE `test-database`', - 'extraStatements' => $extra - ]) - ->shouldBeCalled() - ->willReturn(['name' => 'operations/foo']); - - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->createDatabase( + Argument::that(function ($request) use ($extra) { + $createStatement = 'CREATE DATABASE `test-database`'; + $message = $this->serializer->encodeMessage($request); + $this->assertEquals($message['createStatement'], $createStatement); + $this->assertEquals( + $message['parent'], + InstanceAdminClient::instanceName(self::PROJECT_ID, self::NAME) + ); + $this->assertEquals($message['extraStatements'], $extra); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $database = $this->instance->createDatabase('test-database', [ 'statements' => $extra @@ -400,16 +406,18 @@ public function testCreateDatabase() public function testCreateDatabaseFromBackupName() { $backupName = DatabaseAdminClient::backupName(self::PROJECT_ID, self::NAME, self::BACKUP); - $this->connection->restoreDatabase(Argument::allOf( - Argument::withEntry('databaseId', 'restore-database'), - Argument::withEntry('instance', $this->instance->name()), - Argument::withEntry('backup', $backupName) - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->databaseAdminClient->restoreDatabase( + Argument::that(function ($request) use ($backupName) { + $this->assertEquals($request->getDatabaseId(), 'restore-database'); + $this->assertEquals($request->getParent(), $this->instance->name()); + $this->assertEquals($request->getBackup(), $backupName); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $op = $this->instance->createDatabaseFromBackup('restore-database', $backupName); $this->assertInstanceOf(LongRunningOperation::class, $op); @@ -418,16 +426,18 @@ public function testCreateDatabaseFromBackupName() public function testCreateDatabaseFromBackupObject() { $backupObject = $this->instance->backup(self::BACKUP); - $this->connection->restoreDatabase(Argument::allOf( - Argument::withEntry('databaseId', 'restore-database'), - Argument::withEntry('instance', $this->instance->name()), - Argument::withEntry('backup', $backupObject->name()) - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => 'my-operation' - ]); - $this->instance->___setProperty('connection', $this->connection->reveal()); + + $this->databaseAdminClient->restoreDatabase( + Argument::that(function ($request) use ($backupObject) { + $this->assertEquals($request->getDatabaseId(), 'restore-database'); + $this->assertEquals($request->getParent(), $this->instance->name()); + $this->assertEquals($request->getBackup(), $backupObject->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->operationResponse->reveal()); $op = $this->instance->createDatabaseFromBackup('restore-database', $backupObject); $this->assertInstanceOf(LongRunningOperation::class, $op); @@ -443,17 +453,23 @@ public function testDatabase() public function testDatabases() { $databases = [ - ['name' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::NAME, 'database1')], - ['name' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::NAME, 'database2')] + new DatabaseProto(['name' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::NAME, 'database1')]), + new DatabaseProto(['name' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::NAME, 'database2')]) ]; - $this->connection->listDatabases(Argument::withEntry('instance', $this->instance->name())) - ->shouldBeCalled() - ->willReturn(['databases' => $databases]); + $this->page->getResponseObject()->willReturn(new ListDatabasesResponse(['databases' => $databases])); - $this->connection->getDatabase(Argument::any())->shouldNotBeCalled(); + $this->databaseAdminClient->listDatabases( + Argument::that(function ($request) { + $this->assertEquals($request->getParent(), $this->instance->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->pagedListResponse->reveal()); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->getDatabase(Argument::cetera())->shouldNotBeCalled(); $dbs = $this->instance->databases(); @@ -466,23 +482,65 @@ public function testDatabases() $this->assertEquals('database2', DatabaseAdminClient::parseName($dbs[1]->name())['database']); // Make sure the database->info is prefilled. - $this->assertEquals($databases[0], $dbs[0]->info()); - $this->assertEquals($databases[1], $dbs[1]->info()); + $this->assertEquals( + json_decode($databases[0]->serializeToJsonString(), true), + array_filter($dbs[0]->info()) + ); + $this->assertEquals( + json_decode($databases[1]->serializeToJsonString(), true), + array_filter($dbs[1]->info()) + ); } public function testDatabasesPaged() { $databases = [ - ['name' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::NAME, 'database1')], - ['name' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::NAME, 'database2')] + new DatabaseProto(['name' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::NAME, 'database1')]), + new DatabaseProto(['name' => DatabaseAdminClient::databaseName(self::PROJECT_ID, self::NAME, 'database2')]), ]; - $iteration = 0; - $this->connection->listDatabases(Argument::withEntry('instance', $this->instance->name())) - ->shouldBeCalledTimes(2) - ->willReturn(['databases' => [$databases[0]], 'nextPageToken' => 'foo'], ['databases' => [$databases[1]]]); + $page1 = $this->prophesize(Page::class); + $page1->getResponseObject() + ->willReturn(new ListDatabasesResponse([ + 'databases' => [$databases[0]], 'next_page_token' => 'foo' + ])); + $pagedListResponse1 = $this->prophesize(PagedListResponse::class); + $pagedListResponse1->getPage() + ->willReturn($page1->reveal()); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $iteration = 0; + $this->databaseAdminClient->listDatabases( + Argument::that(function ($request) use (&$iteration) { + $this->assertEquals($request->getParent(), $this->instance->name()); + $iteration++; + return $iteration == 1; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($pagedListResponse1->reveal()); + + $page2 = $this->prophesize(Page::class); + $page2->getResponseObject() + ->willReturn(new ListDatabasesResponse(['databases' => [$databases[1]]])); + $pagedListResponse2 = $this->prophesize(PagedListResponse::class); + $pagedListResponse2->getPage() + ->willReturn($page2->reveal()); + + $this->databaseAdminClient->listDatabases( + Argument::that(function ($request) use (&$iteration) { + $this->assertEquals($request->getParent(), $this->instance->name()); + if ($iteration == 2) { + $this->assertEquals($request->getPageToken(), 'foo'); + return true; + } + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($pagedListResponse2->reveal()); + + $this->databaseAdminClient->getDatabase(Argument::cetera())->shouldNotBeCalled(); $dbs = $this->instance->databases(); @@ -497,7 +555,7 @@ public function testDatabasesPaged() public function testIam() { - $this->assertInstanceOf(Iam::class, $this->instance->iam()); + $this->assertInstanceOf(IamManager::class, $this->instance->iam()); } public function testBackup() @@ -513,19 +571,21 @@ public function testBackup() public function testBackups() { $backups = [ - [ - 'name' => DatabaseAdminClient::backupName(self::PROJECT_ID, self::NAME, 'backup1'), - ], - [ - 'name' => DatabaseAdminClient::backupName(self::PROJECT_ID, self::NAME, 'backup2'), - ] + new BackupProto(['name' => DatabaseAdminClient::backupName(self::PROJECT_ID, self::NAME, 'backup1')]), + new BackupProto(['name' => DatabaseAdminClient::backupName(self::PROJECT_ID, self::NAME, 'backup2')]), ]; - $this->connection->listBackups(Argument::withEntry('instance', $this->instance->name())) - ->shouldBeCalled() - ->willReturn(['backups' => $backups]); + $this->page->getResponseObject()->willReturn(new ListBackupsResponse(['backups' => $backups])); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->listBackups( + Argument::that(function ($request) { + $this->assertEquals($request->getParent(), $this->instance->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->pagedListResponse->reveal()); $bkps = $this->instance->backups(); @@ -541,15 +601,22 @@ public function testBackups() public function testBackupOperations() { $operations = [ - ['name' => 'operation1'], - ['name' => 'operation2'] + new Operation(['name' => 'operation1']), + new Operation(['name' => 'operation2']), ]; - $this->connection->listBackupOperations(Argument::withEntry('instance', $this->instance->name())) - ->shouldBeCalled() - ->willReturn(['operations' => $operations]); + $this->page->getResponseObject()->willReturn(new ListBackupOperationsResponse(['operations' => $operations])); + $this->page->getNextPageToken()->willReturn(null); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->listBackupOperations( + Argument::that(function ($request) { + $this->assertEquals($request->getParent(), $this->instance->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->pagedListResponse->reveal()); $bkpOps = $this->instance->backupOperations(); @@ -564,15 +631,22 @@ public function testBackupOperations() public function testListDatabaseOperations() { $operations = [ - ['name' => 'operation1'], - ['name' => 'operation2'] + new Operation(['name' => 'operation1']), + new Operation(['name' => 'operation2']), ]; - $this->connection->listDatabaseOperations(Argument::withEntry('instance', $this->instance->name())) - ->shouldBeCalled() - ->willReturn(['operations' => $operations]); + $this->page->getResponseObject()->willReturn(new ListDatabaseOperationsResponse(['operations' => $operations])); + $this->page->getNextPageToken()->willReturn(null); - $this->instance->___setProperty('connection', $this->connection->reveal()); + $this->databaseAdminClient->listDatabaseOperations( + Argument::that(function ($request) { + $this->assertEquals($request->getParent(), $this->instance->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->pagedListResponse->reveal()); $dbOps = $this->instance->databaseOperations(); @@ -589,16 +663,29 @@ public function testInstanceDatabaseRole() $sql = 'SELECT * FROM Table'; $database = $this->instance->database($this::DATABASE, ['databaseRole' => 'Reader']); - $this->connection->createSession(Argument::withEntry( - 'session', - ['labels' => [], 'creator_role' => 'Reader'] - )) - ->shouldBeCalled() - ->willReturn([ - 'name' => self::SESSION - ]); - $this->connection->executeStreamingSql(Argument::withEntry('sql', $sql)) - ->shouldBeCalled()->willReturn($this->resultGenerator()); + $this->spannerClient->createSession( + Argument::that(function ($request) { + return $this->serializer->encodeMessage($request)['session']['creatorRole'] + == 'Reader'; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new Session(['name' => self::SESSION])); + + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + return $request->getSql() == $sql; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); + + $this->spannerClient->deleteSession( + Argument::type(DeleteSessionRequest::class), + Argument::type('array') + )->shouldBeCalledOnce(); $database->execute($sql); } @@ -608,20 +695,31 @@ public function testInstanceExecuteWithDirectedRead() $database = $this->instance->database( $this::DATABASE ); - $this->connection->createSession(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => self::SESSION - ]); - - $this->connection->executeStreamingSql(Argument::withEntry( - 'directedReadOptions', - $this->directedReadOptionsIncludeReplicas - )) - ->shouldBeCalled() - ->willReturn( - $this->resultGenerator() - ); + $this->spannerClient->createSession( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new Session(['name' => self::SESSION])); + + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['directedReadOptions'], + $this->directedReadOptionsIncludeReplicas + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); + + $this->spannerClient->deleteSession( + Argument::type(DeleteSessionRequest::class), + Argument::type('array') + )->shouldBeCalledOnce(); $sql = 'SELECT * FROM Table'; $res = $database->execute($sql); @@ -635,23 +733,33 @@ public function testInstanceReadWithDirectedRead() $table = 'foo'; $keys = [10, 'bar']; $columns = ['id', 'name']; - $database = $this->instance->database( - $this::DATABASE, - ); - $this->connection->createSession(Argument::any()) - ->shouldBeCalled() - ->willReturn([ - 'name' => self::SESSION - ]); - - $this->connection->streamingRead(Argument::withEntry( - 'directedReadOptions', - $this->directedReadOptionsIncludeReplicas - )) - ->shouldBeCalled() - ->willReturn( - $this->resultGenerator() - ); + $database = $this->instance->database($this::DATABASE); + + $this->spannerClient->createSession( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new Session(['name' => self::SESSION])); + + $this->spannerClient->streamingRead( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['directedReadOptions'], + $this->directedReadOptionsIncludeReplicas + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); + + $this->spannerClient->deleteSession( + Argument::type(DeleteSessionRequest::class), + Argument::type('array') + )->shouldBeCalledOnce(); $res = $database->read( $table, @@ -667,6 +775,12 @@ public function testInstanceReadWithDirectedRead() private function getDefaultInstance() { - return json_decode(file_get_contents(Fixtures::INSTANCE_FIXTURE()), true); + return new InstanceProto([ + 'name' => 'projects/test-project/instances/instance-name', + 'config' => 'projects/test-project/instanceConfigs/regional-europe-west1', + 'display_name' => 'Instance Name', + 'node_count' => 1, + 'state' => 2 + ]); } } diff --git a/Spanner/tests/Unit/IntervalTest.php b/Spanner/tests/Unit/IntervalTest.php index 99e31421ad8f..a58490c7809e 100644 --- a/Spanner/tests/Unit/IntervalTest.php +++ b/Spanner/tests/Unit/IntervalTest.php @@ -40,64 +40,64 @@ public function testParseString(string $intervalString, string $expected) public function stringsToParseProvider() { return [ - ["P-0Y", "P0Y"], - ["P0Y0M0DT0H0M0S", "P0Y"], - ["P16M", "P1Y4M"], - ["P2Y-2M", "P1Y10M"], - ["P-1Y2M", "P-10M"], - ["P372D", "P372D"], - ["P-372D", "P-372D"], - ["PT7200H", "PT7200H"], - ["PT1H69M72S", "PT2H10M12S"], - ["PT1H-5M-2S", "PT54M58S"], - ["PT0.5S", "PT0.5S"], - ["PT0.500S", "PT0.5S"], - ["PT.5S", "PT0.5S"], - ["P10000Y", "P10000Y"], - ["P-10000Y", "P-10000Y"], - ["P120000M", "P10000Y"], - ["P-120000M", "P-10000Y"], - ["P3660000D", "P3660000D"], - ["P-3660000D", "P-3660000D"], - ["PT316224000000S", "PT87840000H"], - ["PT-316224000000S", "PT-87840000H"], - ["P1Y2M3DT12H12M6.789000123S", "P1Y2M3DT12H12M6.789000123S"], - ["P1Y2M3DT13H-48M6S", "P1Y2M3DT12H12M6S"], - ["P1Y2M3D", "P1Y2M3D"], - ["P1Y2M", "P1Y2M"], - ["P1Y", "P1Y"], - ["P2M", "P2M"], - ["P3D", "P3D"], - ["PT4H25M6.7890001S", "PT4H25M6.7890001S"], - ["PT4H25M6S", "PT4H25M6S"], - ["PT4H30S", "PT4H30S"], - ["PT4H1M", "PT4H1M"], - ["PT5M", "PT5M"], - ["PT6.789S", "PT6.789S"], - ["PT0.123S", "PT0.123S"], - ["PT.000000123S", "PT0.000000123S"], - ["P0Y", "P0Y"], - ["P-1Y-2M-3DT-12H-12M-6.789000123S", "P-1Y-2M-3DT-12H-12M-6.789000123S"], - ["P1Y-2M3DT13H-51M6.789S", "P10M3DT12H9M6.789S"], - ["P-1Y2M-3DT-13H49M-6.789S", "P-10M-3DT-12H-11M-6.789S"], - ["P1Y2M3DT-4H25M-6.7890001S", "P1Y2M3DT-3H-35M-6.7890001S"], - ["PT100H100M100.5S", "PT101H41M40.5S"], - ["P0Y", "P0Y"], - ["PT12H30M1S", "PT12H30M1S"], - ["P1Y2M3D", "P1Y2M3D"], - ["P1Y2M3DT12H30M", "P1Y2M3DT12H30M"], - ["PT0.123456789S", "PT0.123456789S"], - ["PT1H0.5S", "PT1H0.5S"], - ["P1Y2M3DT12H30M1.23456789S", "P1Y2M3DT12H30M1.23456789S"], - ["P1Y2M3DT12H30M1,23456789S", "P1Y2M3DT12H30M1.23456789S"], - ["P-1Y2M3DT12H-30M1.234S", "P-10M3DT11H30M1.234S"], - ["P1Y-2M3DT-12H30M-1.234S", "P10M3DT-11H-30M-1.234S"], - ["PT1.234000S", "PT1.234S"], - ["PT1.000S", "PT1S"], - ["PT87840000H", "PT87840000H"], - ["PT-87840000H", "PT-87840000H"], - ["P2Y1M15DT87839999H59M59.999999999S", "P2Y1M15DT87839999H59M59.999934464S"], - ["P2Y1M15DT-87839999H-59M-59.999999999S", "P2Y1M15DT-87839999H-59M-59.999934464S"] + ['P-0Y', 'P0Y'], + ['P0Y0M0DT0H0M0S', 'P0Y'], + ['P16M', 'P1Y4M'], + ['P2Y-2M', 'P1Y10M'], + ['P-1Y2M', 'P-10M'], + ['P372D', 'P372D'], + ['P-372D', 'P-372D'], + ['PT7200H', 'PT7200H'], + ['PT1H69M72S', 'PT2H10M12S'], + ['PT1H-5M-2S', 'PT54M58S'], + ['PT0.5S', 'PT0.5S'], + ['PT0.500S', 'PT0.5S'], + ['PT.5S', 'PT0.5S'], + ['P10000Y', 'P10000Y'], + ['P-10000Y', 'P-10000Y'], + ['P120000M', 'P10000Y'], + ['P-120000M', 'P-10000Y'], + ['P3660000D', 'P3660000D'], + ['P-3660000D', 'P-3660000D'], + ['PT316224000000S', 'PT87840000H'], + ['PT-316224000000S', 'PT-87840000H'], + ['P1Y2M3DT12H12M6.789000123S', 'P1Y2M3DT12H12M6.789000123S'], + ['P1Y2M3DT13H-48M6S', 'P1Y2M3DT12H12M6S'], + ['P1Y2M3D', 'P1Y2M3D'], + ['P1Y2M', 'P1Y2M'], + ['P1Y', 'P1Y'], + ['P2M', 'P2M'], + ['P3D', 'P3D'], + ['PT4H25M6.7890001S', 'PT4H25M6.7890001S'], + ['PT4H25M6S', 'PT4H25M6S'], + ['PT4H30S', 'PT4H30S'], + ['PT4H1M', 'PT4H1M'], + ['PT5M', 'PT5M'], + ['PT6.789S', 'PT6.789S'], + ['PT0.123S', 'PT0.123S'], + ['PT.000000123S', 'PT0.000000123S'], + ['P0Y', 'P0Y'], + ['P-1Y-2M-3DT-12H-12M-6.789000123S', 'P-1Y-2M-3DT-12H-12M-6.789000123S'], + ['P1Y-2M3DT13H-51M6.789S', 'P10M3DT12H9M6.789S'], + ['P-1Y2M-3DT-13H49M-6.789S', 'P-10M-3DT-12H-11M-6.789S'], + ['P1Y2M3DT-4H25M-6.7890001S', 'P1Y2M3DT-3H-35M-6.7890001S'], + ['PT100H100M100.5S', 'PT101H41M40.5S'], + ['P0Y', 'P0Y'], + ['PT12H30M1S', 'PT12H30M1S'], + ['P1Y2M3D', 'P1Y2M3D'], + ['P1Y2M3DT12H30M', 'P1Y2M3DT12H30M'], + ['PT0.123456789S', 'PT0.123456789S'], + ['PT1H0.5S', 'PT1H0.5S'], + ['P1Y2M3DT12H30M1.23456789S', 'P1Y2M3DT12H30M1.23456789S'], + ['P1Y2M3DT12H30M1,23456789S', 'P1Y2M3DT12H30M1.23456789S'], + ['P-1Y2M3DT12H-30M1.234S', 'P-10M3DT11H30M1.234S'], + ['P1Y-2M3DT-12H30M-1.234S', 'P10M3DT-11H-30M-1.234S'], + ['PT1.234000S', 'PT1.234S'], + ['PT1.000S', 'PT1S'], + ['PT87840000H', 'PT87840000H'], + ['PT-87840000H', 'PT-87840000H'], + ['P2Y1M15DT87839999H59M59.999999999S', 'P2Y1M15DT87839999H59M59.999934464S'], + ['P2Y1M15DT-87839999H-59M-59.999999999S', 'P2Y1M15DT-87839999H-59M-59.999934464S'] ]; } @@ -114,40 +114,40 @@ public function testThrowsOnInvalidData(string $intervalString) public function invalidDataProvider() { return [ - ["P0.5Y"], - ["P0.5M"], - ["P0.5D"], - ["PT0.5H"], - ["PT0.5M"], - ["P5S"], - ["P1Y3S"], - ["P1YT3M1"], - ["P1YT3M1.1.4S"], - [""], - ["P"], - ["PT"], - ["PTS"], - ["PY"], - ["PM"], - ["PD"], - ["PTH"], - ["PTM"], - ["invalid"], - ["P"], - ["PT"], - ["P1YM"], - ["P1Y2M3D4H5M6S"], - ["P1Y2M3DT4H5M6.S"], - ["P1Y2M3DT4H5M6.789SS"], - ["P1Y2M3DT4H5M6."], - ["P1Y2M3DT4H5M6.ABC"], - ["P1Y2M3"], - ["P1Y2M3DT"], - ["PGARBAGET1H"], - ["PT1H-"], - ["P1Y2M3DT4H5M6.789123456789"], - ["P1Y2M3DT4H5M6.123.456S"], - ["P1Y2M3DT4H5M6.,789S"], + ['P0.5Y'], + ['P0.5M'], + ['P0.5D'], + ['PT0.5H'], + ['PT0.5M'], + ['P5S'], + ['P1Y3S'], + ['P1YT3M1'], + ['P1YT3M1.1.4S'], + [''], + ['P'], + ['PT'], + ['PTS'], + ['PY'], + ['PM'], + ['PD'], + ['PTH'], + ['PTM'], + ['invalid'], + ['P'], + ['PT'], + ['P1YM'], + ['P1Y2M3D4H5M6S'], + ['P1Y2M3DT4H5M6.S'], + ['P1Y2M3DT4H5M6.789SS'], + ['P1Y2M3DT4H5M6.'], + ['P1Y2M3DT4H5M6.ABC'], + ['P1Y2M3'], + ['P1Y2M3DT'], + ['PGARBAGET1H'], + ['PT1H-'], + ['P1Y2M3DT4H5M6.789123456789'], + ['P1Y2M3DT4H5M6.123.456S'], + ['P1Y2M3DT4H5M6.,789S'], ]; } @@ -164,12 +164,12 @@ public function testOutOfRangeValues(string $intervalString) public function outOfRangeValuesProvider() { return [ - ["P10001Y"], - ["P-10001Y"], - ["P120001M"], - ["P-120001M"], - ["P3660001D"], - ["P-3660001D"] + ['P10001Y'], + ['P-10001Y'], + ['P120001M'], + ['P-120001M'], + ['P3660001D'], + ['P-3660001D'] ]; } } diff --git a/Spanner/tests/Unit/KeyRangeTest.php b/Spanner/tests/Unit/KeyRangeTest.php index a4da57680852..0082a040de16 100644 --- a/Spanner/tests/Unit/KeyRangeTest.php +++ b/Spanner/tests/Unit/KeyRangeTest.php @@ -17,8 +17,8 @@ namespace Google\Cloud\Spanner\Tests\Unit; -use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Core\Testing\GrpcTestTrait; +use Google\Cloud\Spanner\KeyRange; use InvalidArgumentException; use PHPUnit\Framework\TestCase; @@ -35,7 +35,7 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->range = new KeyRange; + $this->range = new KeyRange(); } public function testConstructWithScalars() diff --git a/Spanner/tests/Unit/KeySetTest.php b/Spanner/tests/Unit/KeySetTest.php index 8b2c772b7840..d76aaf106f6f 100644 --- a/Spanner/tests/Unit/KeySetTest.php +++ b/Spanner/tests/Unit/KeySetTest.php @@ -32,24 +32,24 @@ class KeySetTest extends TestCase public function testAddRange() { - $set = new KeySet; + $set = new KeySet(); $range = $this->prophesize(KeyRange::class); - $range->keyRangeObject()->willReturn('foo'); + $range->keyRangeObject()->willReturn(['foo']); $set->addRange($range->reveal()); - $this->assertEquals('foo', $set->keySetObject()['ranges'][0]); + $this->assertEquals(['foo'], $set->keySetObject()['ranges'][0]); } public function testSetRanges() { - $set = new KeySet; + $set = new KeySet(); $range1 = $this->prophesize(KeyRange::class); - $range1->keyRangeObject()->willReturn('foo'); + $range1->keyRangeObject()->willReturn(['foo']); $range2 = $this->prophesize(KeyRange::class); - $range2->keyRangeObject()->willReturn('bar'); + $range2->keyRangeObject()->willReturn(['bar']); $ranges = [ $range1->reveal(), @@ -58,13 +58,13 @@ public function testSetRanges() $set->setRanges($ranges); - $this->assertEquals('foo', $set->keySetObject()['ranges'][0]); - $this->assertEquals('bar', $set->keySetObject()['ranges'][1]); + $this->assertEquals(['foo'], $set->keySetObject()['ranges'][0]); + $this->assertEquals(['bar'], $set->keySetObject()['ranges'][1]); } public function testAddKey() { - $set = new KeySet; + $set = new KeySet(); $key = 'key'; @@ -75,9 +75,9 @@ public function testAddKey() public function testSetKeys() { - $set = new KeySet; + $set = new KeySet(); - $keys = ['key1','key2']; + $keys = ['key1', 'key2']; $set->setKeys($keys); @@ -86,7 +86,7 @@ public function testSetKeys() public function testSetMatchAll() { - $set = new KeySet; + $set = new KeySet(); $set->setMatchAll(true); $this->assertTrue($set->keySetObject()['all']); @@ -97,7 +97,7 @@ public function testSetMatchAll() public function testRanges() { - $set = new KeySet; + $set = new KeySet(); $range = $this->prophesize(KeyRange::class)->reveal(); $set->addRange($range); @@ -106,7 +106,7 @@ public function testRanges() public function testKeys() { - $set = new KeySet; + $set = new KeySet(); $key = 'foo'; $set->addKey($key); @@ -139,7 +139,7 @@ public function testInvalidAll() public function testFromArray() { $range = new KeyRange(['start' => 'foo', 'end' => 'bar']); - $keys = ['a','b']; + $keys = ['a', 'b']; $all = true; $res = (new KeySet([ 'keys' => $keys, diff --git a/Spanner/tests/Unit/OperationTest.php b/Spanner/tests/Unit/OperationTest.php index 0f8a3def30c7..e646185d6b23 100644 --- a/Spanner/tests/Unit/OperationTest.php +++ b/Spanner/tests/Unit/OperationTest.php @@ -18,30 +18,41 @@ namespace Google\Cloud\Spanner\Tests\Unit; use Google\ApiCore\ServerStream; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Spanner\Batch\QueryPartition; use Google\Cloud\Spanner\Batch\ReadPartition; -use Google\Cloud\Spanner\Connection\Grpc; use Google\Cloud\Spanner\Database; -use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\Snapshot; -use Google\Cloud\Spanner\Tests\StubCreationTrait; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CommitResponse; +use Google\Cloud\Spanner\V1\CommitResponse\CommitStats; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\PartialResultSet; +use Google\Cloud\Spanner\V1\Partition; +use Google\Cloud\Spanner\V1\PartitionResponse; use Google\Cloud\Spanner\V1\ResultSet; +use Google\Cloud\Spanner\V1\ResultSetMetadata; use Google\Cloud\Spanner\V1\ResultSetStats; -use Google\Cloud\Spanner\V1\SpannerClient; +use Google\Cloud\Spanner\V1\StructType; +use Google\Cloud\Spanner\V1\StructType\Field; use Google\Cloud\Spanner\V1\Transaction as TransactionProto; -use Google\Cloud\Spanner\V1\TransactionOptions; -use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode as ReadLockMode; use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; +use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite\ReadLockMode; +use Google\Cloud\Spanner\V1\Type; +use Google\Protobuf\Duration; +use Google\Protobuf\Timestamp as TimestampProto; +use Google\Protobuf\Value; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -53,7 +64,7 @@ class OperationTest extends TestCase { use GrpcTestTrait; use ProphecyTrait; - use StubCreationTrait; + use ApiHelperTrait; const SESSION = 'my-session-id'; const TRANSACTION = 'my-transaction-id'; @@ -61,20 +72,22 @@ class OperationTest extends TestCase const DATABASE = 'projects/my-awesome-project/instances/my-instance/databases/my-database'; const TIMESTAMP = '2017-01-09T18:05:22.534799Z'; - private $connection; private $operation; private $session; + private $spannerClient; + private $serializer; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); + $this->serializer = new Serializer(); + $this->spannerClient = $this->prophesize(SpannerClient::class); - $this->operation = TestHelpers::stub(Operation::class, [ - $this->connection->reveal(), - false - ]); + $this->operation = new Operation( + $this->spannerClient->reveal(), + $this->serializer, + ); $session = $this->prophesize(Session::class); $session->name()->willReturn(self::SESSION); @@ -119,23 +132,29 @@ public function testDeleteMutation() public function testCommit() { - $mutations = [ - $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ - 'foo' => 'bar' - ]) - ]; - - $this->connection->commit(Argument::allOf( - Argument::withEntry('mutations', $mutations), - Argument::withEntry('transactionId', 'foo') - ))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => self::TIMESTAMP - ]); + $mutation = $this->operation->mutation(Operation::OP_INSERT, 'Posts', ['foo' => 'bar']); - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $this->assertEquals('Posts', $request->getMutations()[0]->getInsert()->getTable()); + $this->assertEquals( + $this->serializer->encodeMessage($request->getMutations()[0]->getInsert())['values'], + [['bar']] + ); + $this->assertEquals( + $request->getMutations()[0]->getInsert()->getColumns()[0], + 'foo' + ); + $this->assertEquals(self::TRANSACTION, $request->getTransactionId()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); - $res = $this->operation->commit($this->session, $mutations, [ - 'transactionId' => 'foo' + $res = $this->operation->commit($this->session, [$mutation], [ + 'transactionId' => self::TRANSACTION ]); $this->assertInstanceOf(Timestamp::class, $res); @@ -143,24 +162,23 @@ public function testCommit() public function testCommitWithReturnCommitStats() { - $mutations = [ - $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ - 'foo' => 'bar' - ]) - ]; - - $this->connection->commit(Argument::allOf( - Argument::withEntry('mutations', $mutations), - Argument::withEntry('transactionId', 'foo'), - Argument::withEntry('returnCommitStats', true) - ))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => self::TIMESTAMP, - 'commitStats' => ['mutationCount' => 1] - ]); + $mutation = $this->operation->mutation(Operation::OP_INSERT, 'Posts', ['foo' => 'bar']); - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $this->assertEquals('Posts', $request->getMutations()[0]->getInsert()->getTable()); + $this->assertEquals('foo', $request->getTransactionId()); + $this->assertEquals(true, $request->getReturnCommitStats()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse([ + 'commit_stats' => new CommitStats(['mutation_count' => 1]) + ])); - $res = $this->operation->commitWithResponse($this->session, $mutations, [ + $res = $this->operation->commitWithResponse($this->session, [$mutation], [ 'transactionId' => 'foo', 'returnCommitStats' => true ]); @@ -174,24 +192,29 @@ public function testCommitWithReturnCommitStats() public function testCommitWithMaxCommitDelay() { - $duration = new Duration(0, 100000000); - $mutations = [ - $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ - 'foo' => 'bar' - ]) - ]; + $duration = new Duration(['seconds' => 0, 'nanos' => 100000000]); + $mutation = $this->operation->mutation(Operation::OP_INSERT, 'Posts', ['foo' => 'bar']); - $this->connection->commit(Argument::allOf( - Argument::withEntry('mutations', $mutations), - Argument::withEntry('transactionId', 'foo'), - Argument::withEntry('maxCommitDelay', $duration) - ))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => self::TIMESTAMP, - ]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) use ($duration) { + $this->assertEquals('Posts', $request->getMutations()[0]->getInsert()->getTable()); + $this->assertEquals('foo', $request->getTransactionId()); + $this->assertEquals( + $duration->getSeconds(), + $request->getMaxCommitDelay()->getSeconds() + ); + $this->assertEquals( + $duration->getNanos(), + $request->getMaxCommitDelay()->getNanos() + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); - $res = $this->operation->commitWithResponse($this->session, $mutations, [ + $res = $this->operation->commitWithResponse($this->session, [$mutation], [ 'transactionId' => 'foo', 'maxCommitDelay' => $duration, ]); @@ -204,25 +227,20 @@ public function testCommitWithMaxCommitDelay() public function testCommitWithExistingTransaction() { - $mutations = [ - $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ - 'foo' => 'bar' - ]) - ]; - - $this->connection->commit(Argument::allOf( - Argument::withEntry('mutations', $mutations), - Argument::withEntry('transactionId', self::TRANSACTION), - Argument::that(function ($arg) { - return !isset($arg['singleUseTransaction']); - }) - ))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => self::TIMESTAMP - ]); + $mutation = $this->operation->mutation(Operation::OP_INSERT, 'Posts', ['foo' => 'bar']); - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->commit( + Argument::that(function ($request) { + $this->assertEquals('Posts', $request->getMutations()[0]->getInsert()->getTable()); + $this->assertEquals(self::TRANSACTION, $request->getTransactionId()); + return !$request->hasSingleUseTransaction(); + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); - $res = $this->operation->commit($this->session, $mutations, [ + $res = $this->operation->commit($this->session, [$mutation], [ 'transactionId' => self::TRANSACTION ]); @@ -231,12 +249,14 @@ public function testCommitWithExistingTransaction() public function testRollback() { - $this->connection->rollback(Argument::allOf( - Argument::withEntry('transactionId', self::TRANSACTION), - Argument::withEntry('session', self::SESSION) - ))->shouldBeCalled(); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->rollback( + Argument::that(function ($request) { + $this->assertEquals(self::TRANSACTION, $request->getTransactionId()); + $this->assertEquals(self::SESSION, $request->getSession()); + return true; + }), + Argument::type('array') + )->shouldBeCalledOnce(); $this->operation->rollback($this->session, self::TRANSACTION); } @@ -246,16 +266,22 @@ public function testExecute() $sql = 'SELECT * FROM Posts WHERE ID = @id'; $params = ['id' => 10]; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('session', self::SESSION), - Argument::withEntry('params', ['id' => '10']), - Argument::that(function ($arg) { - return $arg['paramTypes']['id']['code'] === Database::TYPE_INT64; - }) - ))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function ($request) use ($sql) { + $data = $this->serializer->encodeMessage($request); + $this->assertEquals($sql, $request->getSql()); + $this->assertEquals(self::SESSION, $request->getSession()); + $this->assertEquals(['id' => '10'], $data['params']); + $this->assertEquals( + ['id' => ['code' => Database::TYPE_INT64, 'typeAnnotation' => 0, 'protoTypeFqn' => '']], + $data['paramTypes'], + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->executeAndReadResponseStream()); $res = $this->operation->execute($this->session, $sql, [ 'parameters' => $params @@ -268,14 +294,18 @@ public function testExecute() public function testRead() { - $this->connection->streamingRead(Argument::allOf( - Argument::withEntry('table', 'Posts'), - Argument::withEntry('session', self::SESSION), - Argument::withEntry('keySet', ['all' => true]), - Argument::withEntry('columns', ['foo']) - ))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::that(function ($request) { + $this->assertEquals('Posts', $request->getTable()); + $this->assertEquals(self::SESSION, $request->getSession()); + $this->assertTrue($request->getKeySet()->getAll()); + $this->assertEquals(['foo'], $this->serializer->encodeMessage($request)['columns']); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->executeAndReadResponseStream()); $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo']); $this->assertInstanceOf(Result::class, $res); @@ -285,20 +315,23 @@ public function testRead() public function testReadWithTransaction() { - $this->connection->streamingRead(Argument::allOf( - Argument::withEntry('table', 'Posts'), - Argument::withEntry('session', self::SESSION), - Argument::withEntry('keySet', ['all' => true]), - Argument::withEntry('columns', ['foo']) - ))->shouldBeCalled()->willReturn($this->executeAndReadResponse([ - 'transaction' => ['id' => self::TRANSACTION] - ])); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::that(function ($request) { + $this->assertEquals('Posts', $request->getTable()); + $this->assertEquals(self::SESSION, $request->getSession()); + $this->assertTrue($request->getKeySet()->getAll()); + $this->assertEquals(['foo'], $this->serializer->encodeMessage($request)['columns']); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->executeAndReadResponseStream(self::TRANSACTION)); $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()); @@ -307,16 +340,18 @@ public function testReadWithTransaction() public function testReadWithSnapshot() { - $this->connection->streamingRead(Argument::allOf( - Argument::withEntry('table', 'Posts'), - Argument::withEntry('session', self::SESSION), - Argument::withEntry('keySet', ['all' => true]), - Argument::withEntry('columns', ['foo']) - ))->shouldBeCalled()->willReturn($this->executeAndReadResponse([ - 'transaction' => ['id' => self::TRANSACTION] - ])); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::that(function ($request) { + $this->assertEquals('Posts', $request->getTable()); + $this->assertEquals(self::SESSION, $request->getSession()); + $this->assertTrue($request->getKeySet()->getAll()); + $this->assertEquals(['foo'], $this->serializer->encodeMessage($request)['columns']); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->executeAndReadResponseStream(self::TRANSACTION)); $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ 'transactionContext' => SessionPoolInterface::CONTEXT_READ @@ -329,15 +364,15 @@ public function testReadWithSnapshot() public function testTransaction() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('database', self::DATABASE), - Argument::withEntry('session', $this->session->name()), - Argument::withEntry('requestOptions', ['transactionTag' => self::TRANSACTION_TAG]) - )) - ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $this->assertEquals($request->getSession(), $this->session->name()); + return $request->getRequestOptions()->getTransactionTag() == self::TRANSACTION_TAG; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $t = $this->operation->transaction($this->session, ['tag' => self::TRANSACTION_TAG]); $this->assertInstanceOf(Transaction::class, $t); @@ -346,36 +381,38 @@ public function testTransaction() public function testTransactioWithIsolationLevel() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('database', self::DATABASE), - Argument::withEntry('session', $this->session->name()), - Argument::withEntry('isolationLevel', IsolationLevel::REPEATABLE_READ) - )) + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertNotNull($txnOptions = $request->getOptions()); + $this->assertEquals(IsolationLevel::REPEATABLE_READ, $txnOptions->getIsolationLevel()); + return true; + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); - - $options = [ - 'isolationLevel' => IsolationLevel::REPEATABLE_READ - ]; + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $t = $this->operation->transaction($this->session, $options); + $t = $this->operation->transaction($this->session, [ + 'transactionOptions' => ['isolationLevel' => IsolationLevel::REPEATABLE_READ] + ]); $this->assertInstanceOf(Transaction::class, $t); $this->assertEquals(self::TRANSACTION, $t->id()); } public function testTransactionNoTag() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('database', self::DATABASE), - Argument::withEntry('session', $this->session->name()), - Argument::withEntry('requestOptions', []) - )) + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + $this->assertEquals($request->getSession(), $this->session->name()); + $this->assertEquals(0, $request->getRequestOptions()->getPriority()); + $this->assertEquals('', $request->getRequestOptions()->getRequestTag()); + $this->assertEquals('', $request->getRequestOptions()->getTransactionTag()); + return true; + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $t = $this->operation->transaction($this->session); $this->assertInstanceOf(Transaction::class, $t); @@ -384,11 +421,9 @@ public function testTransactionNoTag() public function testTransactionWithExcludeTxnFromChangeStreams() { - $gapic = $this->prophesize(SpannerClient::class); - $gapic->beginTransaction( - self::SESSION, - Argument::that(function (TransactionOptions $options) { - $this->assertTrue($options->getExcludeTxnFromChangeStreams()); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertTrue($request->getOptions()->getExcludeTxnFromChangeStreams()); return true; }), Argument::type('array') @@ -396,12 +431,7 @@ public function testTransactionWithExcludeTxnFromChangeStreams() ->shouldBeCalled() ->willReturn(new TransactionProto(['id' => 'foo'])); - $operation = new Operation( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - true - ); - - $transaction = $operation->transaction($this->session, [ + $transaction = $this->operation->transaction($this->session, [ 'transactionOptions' => ['excludeTxnFromChangeStreams' => true] ]); @@ -416,59 +446,43 @@ public function testExecuteAndExecuteUpdateWithExcludeTxnFromChangeStreams() $stream = $this->prophesize(ServerStream::class); $stream->readAll()->shouldBeCalledTimes(2)->willReturn([$resultSet]); - $gapic = $this->prophesize(SpannerClient::class); - $gapic->executeStreamingSql(self::SESSION, $sql, Argument::that(function (array $options) { - $this->assertArrayHasKey('transaction', $options); - $this->assertNotNull($transactionOptions = $options['transaction']->getBegin()); - $this->assertTrue($transactionOptions->getExcludeTxnFromChangeStreams()); - return true; - })) + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) { + $this->assertTrue($request->getTransaction()->getBegin()->getExcludeTxnFromChangeStreams()); + return true; + }), + Argument::type('array') + ) ->shouldBeCalledTimes(2) ->willReturn($stream->reveal()); - $operation = new Operation( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - true - ); - - $operation->execute($this->session, $sql, [ + $this->operation->execute($this->session, $sql, [ 'transaction' => ['begin' => ['excludeTxnFromChangeStreams' => true]] ]); $transaction = $this->prophesize(Transaction::class)->reveal(); - $operation->executeUpdate($this->session, $transaction, $sql, [ + $this->operation->executeUpdate($this->session, $transaction, $sql, [ 'transaction' => ['begin' => ['excludeTxnFromChangeStreams' => true]] ]); } public function testTransactionWithReadLockMode() { - $expectedReadLockMode = ReadLockMode::OPTIMISTIC; - $gapic = $this->prophesize(SpannerClient::class); - $gapic->beginTransaction( - self::SESSION, - Argument::that(function (TransactionOptions $options) use ($expectedReadLockMode) { - $this->assertNotNull($readWriteTxnOptions = $options->getReadWrite()); - $this->assertNotNull($readLockModeOption = $readWriteTxnOptions->getReadLockMode()); - $this->assertEquals( - $expectedReadLockMode, - $readLockModeOption - ); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertNotNull($txnOptions = $request->getOptions()); + $this->assertNotNull($readWriteTxnOptions = $txnOptions->getReadWrite()); + $this->assertEquals(ReadLockMode::OPTIMISTIC, $readWriteTxnOptions->getReadLockMode()); return true; }), Argument::type('array') ) - ->shouldBeCalled() + ->shouldBeCalledOnce() ->willReturn(new TransactionProto(['id' => 'foo'])); - $operation = new Operation( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - true - ); - - $transaction = $operation->transaction($this->session, [ - 'transactionOptions' => ['readLockMode' => $expectedReadLockMode, ] + $transaction = $this->operation->transaction($this->session, [ + 'transactionOptions' => ['readWrite' => ['readLockMode' => ReadLockMode::OPTIMISTIC]] ]); $this->assertEquals('foo', $transaction->id()); @@ -476,62 +490,50 @@ public function testTransactionWithReadLockMode() public function testExecuteAndExecuteUpdateWithReadLockMode() { - $expectedReadLockMode = ReadLockMode::OPTIMISTIC; $sql = 'SELECT example FROM sql_query'; - $resultSet = new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])]); $stream = $this->prophesize(ServerStream::class); - $stream->readAll()->shouldBeCalledTimes(2)->willReturn([$resultSet]); - - $gapic = $this->prophesize(SpannerClient::class); - $gapic->executeStreamingSql( - self::SESSION, - $sql, - Argument::that(function (array $options) use ($expectedReadLockMode) { - $this->assertArrayHasKey('transaction', $options); - $this->assertNotNull($transactionOptions = $options['transaction']->getBegin()); - $this->assertNotNull($readWriteTxnOptions = $transactionOptions->getReadWrite()); - $this->assertNotNull($readLockModeOption = $readWriteTxnOptions->getReadLockMode()); - $this->assertEquals( - $expectedReadLockMode, - $readLockModeOption - ); + $stream->readAll() + ->shouldBeCalledTimes(2) + ->willReturn([new ResultSet(['stats' => new ResultSetStats(['row_count_exact' => 0])])]); + + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) { + $this->assertNotNull($transaction = $request->getTransaction()); + $this->assertNotNull($txnOptions = $transaction->getBegin()); + $this->assertNotNull($readWriteTxnOptions = $txnOptions->getReadWrite()); + $this->assertEquals(ReadLockMode::OPTIMISTIC, $readWriteTxnOptions->getReadLockMode()); return true; - }) + }), + Argument::type('array') ) ->shouldBeCalledTimes(2) ->willReturn($stream->reveal()); - $operation = new Operation( - new Grpc(['gapicSpannerClient' => $gapic->reveal()]), - true - ); - $lockModeOptions = [ 'transaction' => [ 'begin' => [ - 'readLockMode' => $expectedReadLockMode, + 'readWrite' => ['readLockMode' => ReadLockMode::OPTIMISTIC], ] ] ]; - $operation->execute($this->session, $sql, $lockModeOptions); + $this->operation->execute($this->session, $sql, $lockModeOptions); $transaction = $this->prophesize(Transaction::class)->reveal(); - - $operation->executeUpdate($this->session, $transaction, $sql, $lockModeOptions); + $this->operation->executeUpdate($this->session, $transaction, $sql, $lockModeOptions); } public function testSnapshot() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('database', self::DATABASE), - Argument::withEntry('session', $this->session->name()) - )) + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + return $request->getSession() == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); $snap = $this->operation->snapshot($this->session); $this->assertInstanceOf(Snapshot::class, $snap); @@ -541,10 +543,7 @@ public function testSnapshot() public function testSnapshotSingleUse() { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotBeCalled(); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); $snap = $this->operation->snapshot($this->session, ['singleUse' => true]); $this->assertInstanceOf(Snapshot::class, $snap); @@ -554,14 +553,17 @@ public function testSnapshotSingleUse() public function testSnapshotWithTimestamp() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('database', self::DATABASE), - Argument::withEntry('session', $this->session->name()) - )) + $this->spannerClient->beginTransaction( + Argument::that(function ($request) { + return $request->getSession() == $this->session->name(); + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn(['id' => self::TRANSACTION, 'readTimestamp' => self::TIMESTAMP]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + ->willReturn(new TransactionProto([ + 'id' => self::TRANSACTION, + 'read_timestamp' => new TimestampProto(['seconds' => (new \DateTime(self::TIMESTAMP))->format('U')]) + ])); $snap = $this->operation->snapshot($this->session); $this->assertInstanceOf(Snapshot::class, $snap); @@ -578,28 +580,27 @@ public function testPartitionQuery() $partitionToken1 = 'token1'; $partitionToken2 = 'token2'; - $this->connection->partitionQuery(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('session', self::SESSION), - Argument::withEntry('params', ['id' => '10']), - Argument::that(function ($arg) use ($transactionId) { - if ($arg['paramTypes']['id']['code'] !== Database::TYPE_INT64) { - return false; - } - - return $arg['transactionId'] === $transactionId; - }) - ))->shouldBeCalled()->willReturn([ - 'partitions' => [ - [ - 'partitionToken' => $partitionToken1 - ], [ - 'partitionToken' => $partitionToken2 + $this->spannerClient->partitionQuery( + Argument::that(function ($request) use ($sql, $transactionId, $partitionToken1, $partitionToken2) { + $this->assertEquals($request->getSql(), $sql); + $this->assertEquals(self::SESSION, $request->getSession()); + $this->assertEquals( + ['id' => '10'], + json_decode($request->getParams()->serializeToJsonString(), true) + ); + $this->assertEquals(Database::TYPE_INT64, $request->getParamTypes()['id']->getCode()); + $this->assertEquals($transactionId, $request->getTransaction()->getId()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalled() + ->willReturn(new PartitionResponse([ + 'partitions' => [ + new Partition(['partition_token' => $partitionToken1]), + new Partition(['partition_token' => $partitionToken2]), ] - ] - ]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + ])); $res = $this->operation->partitionQuery($this->session, $transactionId, $sql, [ 'parameters' => $params @@ -613,39 +614,36 @@ public function testPartitionQuery() public function testPartitionRead() { - $sql = 'SELECT * FROM Posts WHERE ID = @id'; $params = ['id' => 10]; $transactionId = 'foo'; $partitionToken1 = 'token1'; $partitionToken2 = 'token2'; - $this->connection->partitionRead(Argument::allOf( - Argument::withEntry('table', 'Posts'), - Argument::withEntry('session', self::SESSION), - Argument::withEntry('keySet', ['all' => true]), - Argument::withEntry('columns', ['foo']) - ))->shouldBeCalled()->willReturn([ - 'partitions' => [ - [ - 'partitionToken' => $partitionToken1 - ], [ - 'partitionToken' => $partitionToken2 + $this->spannerClient->partitionRead( + Argument::that(function ($request) { + $this->assertEquals('Posts', $request->getTable()); + $this->assertEquals(self::SESSION, $request->getSession()); + $this->assertEquals(true, $request->getKeySet()->getAll()); + $this->assertEquals(['foo'], $this->serializer->encodeMessage($request)['columns']); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalled() + ->willReturn(new PartitionResponse([ + 'partitions' => [ + new Partition(['partition_token' => $partitionToken1]), + new Partition(['partition_token' => $partitionToken2]), ] - ] - ]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); + ])); $res = $this->operation->partitionRead( $this->session, $transactionId, 'Posts', new KeySet(['all' => true]), - ['foo'], - [ - 'parameters' => $params - ] + ['foo'] ); $this->assertContainsOnlyInstancesOf(ReadPartition::class, $res); @@ -654,24 +652,44 @@ public function testPartitionRead() $this->assertEquals($partitionToken2, $res[1]->token()); } - private function executeAndReadResponse(array $additionalMetadata = []) + private function executeAndReadResponseStream(?string $transactionId = null) + { + $stream = $this->prophesize(ServerStream::class); + $stream->readAll()->willReturn($this->executeAndReadResponse($transactionId)); + + return $stream->reveal(); + } + + private function executeAndReadResponse(?string $transactionId = null) { - yield [ - 'metadata' => array_merge([ - 'rowType' => [ + $transactionMetadata = []; + if ($transactionId) { + $transactionMetadata = ['transaction' => new TransactionProto(['id' => $transactionId])]; + } + yield new PartialResultSet([ + 'metadata' => new ResultSetMetadata([ + 'row_type' => new StructType([ 'fields' => [ - [ + new Field([ 'name' => 'ID', - 'type' => [ - 'code' => Database::TYPE_INT64 - ] - ] + 'type' => new Type(['code' => Database::TYPE_INT64]) + ]), ] - ] - ], $additionalMetadata), + ]) + ] + $transactionMetadata), 'values' => [ - '10' + new Value(['string_value' => '10']) ] - ]; + ]); + } + + private function commitResponse($commit = []) + { + return new CommitResponse($commit + [ + 'commit_timestamp' => new TimestampProto([ + 'seconds' => (new \DateTime(self::TIMESTAMP))->format('U'), + 'nanos' => 534799000 + ]) + ]); } } diff --git a/Spanner/tests/Unit/PgJsonbTest.php b/Spanner/tests/Unit/PgJsonbTest.php index 59bc844aabd0..0450f6be6384 100644 --- a/Spanner/tests/Unit/PgJsonbTest.php +++ b/Spanner/tests/Unit/PgJsonbTest.php @@ -46,7 +46,7 @@ public function validValueProvider() { $obj = $this->prophesize('stdClass'); $obj->willImplement('JsonSerializable'); - $obj->jsonSerialize()->willReturn(["a" => 1, "b" => null]); + $obj->jsonSerialize()->willReturn(['a' => 1, 'b' => null]); return [ @@ -56,7 +56,7 @@ public function validValueProvider() // // null value shouldn't be casted [null, null], // // arrays should be converted to JSON - [["a"=>1.1, "b"=>"2"], '{"a":1.1,"b":"2"}'], + [['a' => 1.1, 'b' => '2'], '{"a":1.1,"b":"2"}'], // JsonSerializable should be used after a json_encode call [$obj->reveal(), '{"a":1,"b":null}'] ]; diff --git a/Spanner/tests/Unit/PgNumericTest.php b/Spanner/tests/Unit/PgNumericTest.php index 71cbd3d9542f..38671301fae5 100644 --- a/Spanner/tests/Unit/PgNumericTest.php +++ b/Spanner/tests/Unit/PgNumericTest.php @@ -18,8 +18,8 @@ namespace Google\Cloud\Spanner\Tests\Unit; use Google\Cloud\Spanner\PgNumeric; -use Google\Cloud\Spanner\V1\TypeCode; use Google\Cloud\Spanner\V1\TypeAnnotationCode; +use Google\Cloud\Spanner\V1\TypeCode; use PHPUnit\Framework\TestCase; /** diff --git a/Spanner/tests/Unit/ResultTest.php b/Spanner/tests/Unit/ResultTest.php index 05d8f17ac8e5..f4dc9f175a13 100644 --- a/Spanner/tests/Unit/ResultTest.php +++ b/Spanner/tests/Unit/ResultTest.php @@ -18,11 +18,14 @@ namespace Google\Cloud\Spanner\Tests\Unit; use Google\Cloud\Core\Exception\ServiceException; -use Google\Cloud\Spanner\Transaction; +use Google\Cloud\Core\Testing\GrpcTestTrait; +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\Tests\ResultGeneratorTrait; +use Google\Cloud\Spanner\Transaction; use Google\Cloud\Spanner\ValueMapper; -use Google\Cloud\Core\Testing\GrpcTestTrait; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -35,9 +38,9 @@ class ResultTest extends TestCase { use GrpcTestTrait; use ProphecyTrait; - use ResultTestTrait; + use ResultGeneratorTrait; - private $metadata = [ + private const METADATA = [ 'rowType' => [ 'fields' => [ [ @@ -47,10 +50,29 @@ class ResultTest extends TestCase ] ] ]; + private $operation; + private $session; + private $transaction; + private $snapshot; + private $mapper; public function setUp(): void { $this->checkAndSkipGrpcTests(); + + $this->operation = $this->prophesize(Operation::class); + $this->session = $this->prophesize(Session::class); + $this->transaction = $this->prophesize(Transaction::class); + $this->snapshot = $this->prophesize(Snapshot::class); + + $this->mapper = $this->prophesize(ValueMapper::class); + $this->mapper->decodeValues( + Argument::any(), + Argument::any(), + Argument::any() + )->will(function ($args) { + return $args[1]; + }); } /** @@ -58,31 +80,47 @@ public function setUp(): void */ public function testRows($chunks, $expectedValues) { - $result = iterator_to_array($this->getResultClass($chunks)->rows()); - $this->assertEquals($expectedValues, $result); + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($chunks) { + return $this->resultGeneratorJson($chunks); + }, + 'r', + $this->mapper->reveal() + ); + $this->assertEquals($expectedValues, iterator_to_array($result->rows())); } public function testIterator() { - $fixture = $this->getStreamingDataFixture()['tests'][0]; - $result = iterator_to_array($this->getResultClass($fixture['chunks'])); + $fixtures = $this->getStreamingDataFixture()['tests'][0]; + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixtures) { + return $this->resultGeneratorJson($fixtures['chunks']); + }, + 'r', + $this->mapper->reveal() + ); - $this->assertEquals($fixture['result']['value'], $result); + $this->assertEquals($fixtures['result']['value'], iterator_to_array($result)); } public function testFailsWhenStreamThrowsUnrecoverableException() { $this->expectException(\Exception::class); - $result = $this->getResultClass( - null, - 'r', - null, + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), function () { - throw new \Exception; - } + throw new \Exception(); + }, + 'r', + $this->mapper->reveal() ); - iterator_to_array($result->rows()); } @@ -91,22 +129,19 @@ public function testResumesBrokenStream() $timesCalled = 0; $chunks = [ [ - 'metadata' => $this->metadata, + 'metadata' => self::METADATA, 'values' => ['a'] ], [ 'values' => ['b'], 'resumeToken' => 'abc' ], - [ - 'values' => ['c'] - ] + ['values' => ['c']] ]; - $result = $this->getResultClass( - null, - 'r', - null, + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), function () use ($chunks, &$timesCalled) { $timesCalled++; @@ -116,7 +151,9 @@ function () use ($chunks, &$timesCalled) { } yield $chunk; } - } + }, + 'r', + $this->mapper->reveal() ); iterator_to_array($result->rows()); @@ -128,21 +165,16 @@ public function testResumesAfterStreamStartFailure() $timesCalled = 0; $chunks = [ [ - 'metadata' => $this->metadata, + 'metadata' => self::METADATA, 'values' => ['a'] ], - [ - 'values' => ['b'] - ], - [ - 'values' => ['c'] - ] + ['values' => ['b']], + ['values' => ['c']] ]; - $result = $this->getResultClass( - null, - 'r', - null, + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), function () use ($chunks, &$timesCalled) { $timesCalled++; if ($timesCalled === 1) { @@ -152,7 +184,9 @@ function () use ($chunks, &$timesCalled) { foreach ($chunks as $key => $chunk) { yield $chunk; } - } + }, + 'r', + $this->mapper->reveal() ); iterator_to_array($result->rows()); @@ -164,21 +198,16 @@ public function testRowsRetriesWithoutResumeTokenWhenNotYieldedRows() $timesCalled = 0; $chunks = [ [ - 'metadata' => $this->metadata, + 'metadata' => self::METADATA, 'values' => ['a'] ], - [ - 'values' => ['b'] - ], - [ - 'values' => ['c'] - ] + ['values' => ['b']], + ['values' => ['c']] ]; - $result = $this->getResultClass( - null, - 'r', - null, + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), function () use ($chunks, &$timesCalled) { $timesCalled++; foreach ($chunks as $key => $chunk) { @@ -187,7 +216,9 @@ function () use ($chunks, &$timesCalled) { } yield $chunk; } - } + }, + 'r', + $this->mapper->reveal() ); iterator_to_array($result->rows()); @@ -199,22 +230,17 @@ public function testRowsRetriesWithResumeTokenWhenNotYieldedRows() $timesCalled = 0; $chunks = [ [ - 'metadata' => $this->metadata, + 'metadata' => self::METADATA, 'values' => ['a'], 'resumeToken' => 'abc' ], - [ - 'values' => ['b'] - ], - [ - 'values' => ['c'] - ] + ['values' => ['b']], + ['values' => ['c']] ]; - $result = $this->getResultClass( - null, - 'r', - null, + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), function () use ($chunks, &$timesCalled) { $timesCalled++; foreach ($chunks as $key => $chunk) { @@ -223,7 +249,9 @@ function () use ($chunks, &$timesCalled) { } yield $chunk; } - } + }, + 'r', + $this->mapper->reveal() ); iterator_to_array($result->rows()); @@ -236,18 +264,15 @@ public function testThrowsExceptionWhenCannotRetry() $chunks = [ [ - 'metadata' => $this->metadata, + 'metadata' => self::METADATA, 'values' => ['a'] ], - [ - 'values' => ['b'] - ] + ['values' => ['b']] ]; - $result = $this->getResultClass( - null, - 'r', - null, + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), function () use ($chunks) { foreach ($chunks as $key => $chunk) { if ($key === 1) { @@ -255,7 +280,9 @@ function () use ($chunks) { } yield $chunk; } - } + }, + 'r', + $this->mapper->reveal() ); iterator_to_array($result->rows()); @@ -264,7 +291,15 @@ function () use ($chunks) { public function testColumns() { $fixture = $this->getStreamingDataFixture()['tests'][0]; - $result = $this->getResultClass($fixture['chunks']); + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixture) { + return $this->resultGeneratorJson($fixture['chunks']); + }, + 'r', + $this->mapper->reveal() + ); $expectedColumnNames = ['f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7']; $this->assertNull($result->columns()); @@ -275,7 +310,15 @@ public function testColumns() public function testMetadata() { $fixture = $this->getStreamingDataFixture()['tests'][0]; - $result = $this->getResultClass($fixture['chunks']); + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixture) { + return $this->resultGeneratorJson($fixture['chunks']); + }, + 'r', + $this->mapper->reveal() + ); $expectedMetadata = json_decode($fixture['chunks'][0], true)['metadata']; $this->assertNull($result->stats()); @@ -286,7 +329,15 @@ public function testMetadata() public function testSession() { $fixture = $this->getStreamingDataFixture()['tests'][0]; - $result = $this->getResultClass($fixture['chunks']); + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixture) { + return $this->resultGeneratorJson($fixture['chunks']); + }, + 'r', + $this->mapper->reveal() + ); $this->assertInstanceOf(Session::class, $result->session()); } @@ -294,7 +345,15 @@ public function testSession() public function testStats() { $fixture = $this->getStreamingDataFixture()['tests'][1]; - $result = $this->getResultClass($fixture['chunks']); + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixture) { + return $this->resultGeneratorJson($fixture['chunks']); + }, + 'r', + $this->mapper->reveal() + ); $expectedStats = json_decode($fixture['chunks'][0], true)['stats']; $this->assertNull($result->stats()); @@ -305,7 +364,15 @@ public function testStats() public function testTransaction() { $fixture = $this->getStreamingDataFixture()['tests'][1]; - $result = $this->getResultClass($fixture['chunks'], 'rw'); + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixture) { + return $this->resultGeneratorJson($fixture['chunks']); + }, + 'rw', + $this->mapper->reveal() + ); $this->assertInstanceOf(Transaction::class, $result->transaction()); } @@ -313,7 +380,15 @@ public function testTransaction() public function testSnapshot() { $fixture = $this->getStreamingDataFixture()['tests'][1]; - $result = $this->getResultClass($fixture['chunks']); + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixture) { + return $this->resultGeneratorJson($fixture['chunks']); + }, + 'r', + $this->mapper->reveal() + ); $this->assertInstanceOf(Snapshot::class, $result->snapshot()); } @@ -327,7 +402,16 @@ public function testUsesCorrectDefaultFormatOption() Argument::any(), 'associative' )->shouldBeCalled(); - $result = $this->getResultClass($fixture['chunks'], 'r', $mapper->reveal()); + + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixture) { + return $this->resultGeneratorJson($fixture['chunks']); + }, + 'r', + $mapper->reveal() + ); $rows = $result->rows(); $rows->current(); @@ -345,7 +429,16 @@ public function testRecievesCorrectFormatOption($format) Argument::any(), $format )->shouldBeCalled(); - $result = $this->getResultClass($fixture['chunks'], 'r', $mapper->reveal()); + + $result = new Result( + $this->operation->reveal(), + $this->session->reveal(), + function () use ($fixture) { + return $this->resultGeneratorJson($fixture['chunks']); + }, + 'r', + $mapper->reveal() + ); $rows = $result->rows($format); $rows->current(); diff --git a/Spanner/tests/Unit/ResultTestTrait.php b/Spanner/tests/Unit/ResultTestTrait.php deleted file mode 100644 index 713fa4a14d01..000000000000 --- a/Spanner/tests/Unit/ResultTestTrait.php +++ /dev/null @@ -1,112 +0,0 @@ -<?php -/** - * Copyright 2016 Google Inc. - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit; - -use Google\Cloud\Spanner\Transaction; -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\ValueMapper; -use Prophecy\Argument; -use Prophecy\PhpUnit\ProphecyTrait; - -trait ResultTestTrait -{ - use ProphecyTrait; - - public function streamingDataProvider() - { - foreach ($this->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 = null, - $context = 'r', - $mapper = null, - $call = null - ) { - $operation = $this->prophesize(Operation::class); - $session = $this->prophesize(Session::class)->reveal(); - $transaction = $this->prophesize(Transaction::class); - $snapshot = $this->prophesize(Snapshot::class); - - 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( - $session, - Argument::type('array') - )->willReturn($snapshot->reveal()); - } else { - $operation->createTransaction( - $session, - Argument::type('array') - )->willReturn($transaction->reveal()); - } - - return new Result( - $operation->reveal(), - $session, - $call, - $context, - $mapper - ); - } - - private function resultGenerator($chunks) - { - foreach ($chunks as $chunk) { - yield json_decode($chunk, true); - } - } - - private function getStreamingDataFixture() - { - return json_decode( - file_get_contents(Fixtures::STREAMING_READ_ACCEPTANCE_FIXTURE()), - true - ); - } -} diff --git a/Spanner/tests/Unit/Session/CacheSessionPoolTest.php b/Spanner/tests/Unit/Session/CacheSessionPoolTest.php index 3461bf4a6d8a..20fbbdf9e720 100644 --- a/Spanner/tests/Unit/Session/CacheSessionPoolTest.php +++ b/Spanner/tests/Unit/Session/CacheSessionPoolTest.php @@ -18,20 +18,23 @@ namespace Google\Cloud\Spanner\Tests\Unit\Session; use Google\Auth\Cache\MemoryCacheItemPool; +use Google\Cloud\Core\RequestHandler; +use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\Testing\Lock\MockValues; -use Google\Cloud\Spanner\Connection\Grpc; use Google\Cloud\Spanner\Database; +use Google\Cloud\Spanner\Result; use Google\Cloud\Spanner\Session\CacheSessionPool; use Google\Cloud\Spanner\Session\Session; -use Google\Cloud\Core\Testing\GrpcTestTrait; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; +use Google\Protobuf\GPBEmpty; use GuzzleHttp\Promise\FulfilledPromise; use GuzzleHttp\Promise\RejectedPromise; -use Psr\Cache\CacheItemInterface; -use Psr\Cache\CacheItemPoolInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\Argument\ArgumentsWildcard; use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface; use ReflectionMethod; /** @@ -42,6 +45,7 @@ class CacheSessionPoolTest extends TestCase { use GrpcTestTrait; use ProphecyTrait; + use ResultGeneratorTrait; const CACHE_KEY_TEMPLATE = CacheSessionPool::CACHE_KEY_TEMPLATE; const PROJECT_ID = 'project'; @@ -84,7 +88,7 @@ public function badConfigDataProvider() [['maxCyclesToWaitForSession' => -1]], [['sleepIntervalSeconds' => -1]], [['minSessions' => 5, 'maxSessions' => 1]], - [['lock' => new \stdClass]] + [['lock' => new \stdClass()]] ]; } @@ -174,7 +178,7 @@ public function testAcquireThrowsExceptionWithNoAvailableSessions() public function testAcquireRemovesToCreateItemsIfCreateCallFails() { $exceptionThrown = false; - $config = ['maxSessions' => 1]; + $config = ['maxSessions' => 1, 'sleepIntervalSeconds' => 0]; $pool = new CacheSessionPoolStub($this->getCacheItemPool(), $config, $this->time); $pool->setDatabase($this->getDatabase(true)); @@ -195,9 +199,10 @@ public function testAcquireRemovesToCreateItemsIfCreateCallFails() public function testAcquireIfCreateSessionCallFails() { + $config = ['sleepIntervalSeconds' => 0]; $exceptionThrown = false; $exceptionMessage = null; - $pool = new CacheSessionPoolStub($this->getCacheItemPool()); + $pool = new CacheSessionPoolStub($this->getCacheItemPool(), $config); $pool->setDatabase($this->getDatabase(true)); try { @@ -855,29 +860,25 @@ public function acquireDataProvider() private function getDatabase($shouldCreateFails = false, $willDeleteSessions = false, $expectedCreateCalls = null) { - $database = $this->prophesize(DatabaseStub::class); - $session = $this->prophesize(SessionStub::class); - $connection = $this->prophesize(Grpc::class); + $database = $this->prophesize(Database::class); + $session = $this->prophesize(Session::class); + $requestHandler = $this->prophesize(RequestHandler::class); + $result = $this->prophesize(Result::class); + $result->rows()->willReturn($this->resultGeneratorJson([])); $session->expiration() ->willReturn($this->time + 3600); $session->exists() ->willReturn(false); if ($willDeleteSessions) { - $session->delete() - ->willReturn(null); - $connection->deleteSessionAsync(Argument::any()) - ->willReturn(new FulfilledPromise( - new DumbObject() - )); + $session->delete(); + $database->deleteSessionAsync(Argument::any()) + ->willReturn(new FulfilledPromise(new GPBEmpty())); } else { - $connection->deleteSessionAsync(Argument::any()) - ->willReturn(new RejectedPromise( - new DumbObject() - )); + $database->deleteSessionAsync(Argument::any()) + ->willReturn(new RejectedPromise(new GPBEmpty())); } - $database->connection() - ->willReturn($connection->reveal()); + $database->session(Argument::any()) ->will(function ($args) use ($session) { $session->name() @@ -894,11 +895,11 @@ private function getDatabase($shouldCreateFails = false, $willDeleteSessions = f $database->name() ->willReturn(self::DATABASE_NAME); $database->execute(Argument::exact('SELECT 1'), Argument::withKey('session')) - ->willReturn(new DumbObject); + ->willReturn($result->reveal()); $createRes = function ($args, $mock, $method) use ($shouldCreateFails) { if ($shouldCreateFails) { - throw new \Exception("error"); + throw new \Exception('error'); } $methodCalls = $mock->findProphecyMethodCalls( @@ -916,11 +917,11 @@ private function getDatabase($shouldCreateFails = false, $willDeleteSessions = f }; if ($expectedCreateCalls) { - $connection->batchCreateSessions(Argument::any()) + $database->batchCreateSessions(Argument::any()) ->shouldBeCalledTimes($expectedCreateCalls) ->will($createRes); } else { - $connection->batchCreateSessions(Argument::any()) + $database->batchCreateSessions(Argument::any()) ->will($createRes); } @@ -1001,7 +1002,7 @@ public function testMaintainEmptyData() public function testMaintainException() { $data = $this->cacheData(['dead' => 3700, 'old' => 3200, 'fresh' => 100, 'other' => 1500], 300); - $database = $this->prophesize(DatabaseStub::class); + $database = $this->prophesize(Database::class); $database->identity()->willReturn([ 'projectId' => self::PROJECT_ID, 'database' => self::DATABASE_NAME, @@ -1049,7 +1050,7 @@ public function testMaintainServerDeletedSessions( $data = [] ) { $cacheData = $this->cacheData($initialItems, $maintainInterval); - $expiredTime = $this->time - 28*24*60*60; // 28 days + $expiredTime = $this->time - 28 * 24 * 60 * 60; // 28 days foreach ($cacheData['queue'] as $k => $v) { $cacheData['queue'][$k]['creation'] = $expiredTime; } @@ -1179,7 +1180,7 @@ public function testSessionPoolDatabaseRole() $config = ['minSessions' => 1, 'databaseRole' => 'Reader']; $cache = $this->getCacheItemPool($initialData); $pool = new CacheSessionPoolStub($cache, $config, $this->time); - $database = $this->prophesize(DatabaseStub::class); + $database = $this->prophesize(Database::class); $database->identity() ->willReturn([ 'projectId' => self::PROJECT_ID, @@ -1188,13 +1189,10 @@ public function testSessionPoolDatabaseRole() ]); $database->name() ->willReturn(self::DATABASE_NAME); - $connection = $this->prophesize(Grpc::class); - $connection->batchCreateSessions(['database' => self::DATABASE_NAME, + $database->batchCreateSessions([ 'sessionTemplate' => ['labels' => [], 'creator_role' => 'Reader'], 'sessionCount' => 1]) ->shouldBeCalled() - ->willReturn(['session' => array(['name' => 'session', 'expirtation' => $this->time])]); - $database->connection() - ->willReturn($connection->reveal()); + ->willReturn(['session' => [['name' => 'session', 'expirtation' => $this->time]]]); $pool->setDatabase($database->reveal()); $pool->warmup(); @@ -1217,40 +1215,4 @@ protected function time() return $this->time ?: parent::time(); } } - -class DatabaseStub extends Database -{ - // prevent "get_class() expects parameter 1 to be object" warning when debugging - public function __debugInfo() - { - return []; - } -} - -class SessionStub extends Session -{ - // prevent "get_class() expects parameter 1 to be object" warning when debugging - public function __debugInfo() - { - return []; - } -} - -class DumbObject -{ - public function __get($name) - { - return $this; - } - - public function __call($name, $args) - { - return $this; - } - - public function serializeToString() - { - return ''; - } -} //@codingStandardsIgnoreEnd diff --git a/Spanner/tests/Unit/SnapshotTest.php b/Spanner/tests/Unit/SnapshotTest.php index 04194e5b0c98..e88849a8e50a 100644 --- a/Spanner/tests/Unit/SnapshotTest.php +++ b/Spanner/tests/Unit/SnapshotTest.php @@ -17,17 +17,17 @@ namespace Google\Cloud\Spanner\Tests\Unit; +use Google\Cloud\Core\Testing\GrpcTestTrait; +use Google\Cloud\Spanner\KeySet; 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\Timestamp; -use Google\Cloud\Core\Testing\GrpcTestTrait; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; -use Google\Cloud\Spanner\KeySet; -use Google\Cloud\Spanner\Result; /** * @group spanner @@ -45,7 +45,7 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->timestamp = new Timestamp(new \DateTime); + $this->timestamp = new Timestamp(new \DateTime()); $args = [ 'id' => 'foo', diff --git a/Spanner/tests/Unit/SpannerClientTest.php b/Spanner/tests/Unit/SpannerClientTest.php index c3751f707ae0..7b1a8fefd114 100644 --- a/Spanner/tests/Unit/SpannerClientTest.php +++ b/Spanner/tests/Unit/SpannerClientTest.php @@ -17,31 +17,39 @@ namespace Google\Cloud\Spanner\Tests\Unit; +use Google\ApiCore\OperationResponse; +use Google\ApiCore\Page; +use Google\ApiCore\PagedListResponse; use Google\Cloud\Core\Int64; use Google\Cloud\Core\Iterator\ItemIterator; use Google\Cloud\Core\LongRunning\LongRunningOperation; use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Core\Testing\TestHelpers; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Core\Testing\Snippet\Fixtures; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Instance as InstanceProto; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceConfig; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstanceConfigsResponse; +use Google\Cloud\Spanner\Admin\Instance\V1\ListInstancesResponse; use Google\Cloud\Spanner\Batch\BatchClient; use Google\Cloud\Spanner\Bytes; use Google\Cloud\Spanner\CommitTimestamp; 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\PgJsonb; -use Google\Cloud\Spanner\PgOid; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Numeric; +use Google\Cloud\Spanner\PgJsonb; use Google\Cloud\Spanner\PgNumeric; +use Google\Cloud\Spanner\PgOid; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\SpannerClient; -use Google\Cloud\Spanner\Tests\StubCreationTrait; use Google\Cloud\Spanner\Timestamp; +use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient; use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; +use Google\Protobuf\Duration; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -55,22 +63,24 @@ class SpannerClientTest extends TestCase { use GrpcTestTrait; use ProphecyTrait; - use StubCreationTrait; const PROJECT = 'my-awesome-project'; const INSTANCE = 'inst'; const DATABASE = 'db'; const CONFIG = 'conf'; - private $client; - private $connection; + private $serializer; + private SpannerClient $spannerClient; + private $instanceAdminClient; private $directedReadOptionsIncludeReplicas; + private $operationResponse; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); + $this->serializer = new Serializer(); + $this->directedReadOptionsIncludeReplicas = [ 'includeReplicas' => [ 'replicaSelections' => [ @@ -80,17 +90,21 @@ public function setUp(): void ] ] ]; - $this->client = TestHelpers::stub(SpannerClient::class, [ - [ - 'projectId' => self::PROJECT, - 'directedReadOptions' => $this->directedReadOptionsIncludeReplicas, - ] + + $this->instanceAdminClient = $this->prophesize(InstanceAdminClient::class); + $this->spannerClient = new SpannerClient([ + 'projectId' => self::PROJECT, + 'credentials' => Fixtures::KEYFILE_STUB_FIXTURE(), + 'directedReadOptions' => $this->directedReadOptionsIncludeReplicas, + 'gapicSpannerInstanceAdminClient' => $this->instanceAdminClient->reveal() ]); + + $this->operationResponse = $this->prophesize(OperationResponse::class); } public function testBatch() { - $batch = $this->client->batch('foo', 'bar'); + $batch = $this->spannerClient->batch('foo', 'bar'); $this->assertInstanceOf(BatchClient::class, $batch); $ref = new \ReflectionObject($batch); @@ -98,8 +112,7 @@ public function testBatch() $prop->setAccessible(true); $this->assertEquals( - sprintf( - 'projects/%s/instances/%s/databases/%s', + GapicSpannerClient::databaseName( self::PROJECT, 'foo', 'bar' @@ -113,25 +126,34 @@ public function testBatch() */ public function testInstanceConfigurations() { - $this->connection->listInstanceConfigs( - Argument::withEntry('projectName', InstanceAdminClient::projectName(self::PROJECT)) - ) - ->shouldBeCalled() - ->willReturn([ - 'instanceConfigs' => [ - [ + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListInstanceConfigsResponse([ + 'instance_configs' => [ + new InstanceConfig([ 'name' => InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG), - 'displayName' => 'Bar' - ], [ + 'display_name' => 'Bar' + ]), + new InstanceConfig([ 'name' => InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG), - 'displayName' => 'Bat' - ] + 'display_name' => 'Bat' + ]), ] - ]); - - $this->client->___setProperty('connection', $this->connection->reveal()); + ])); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + + $this->instanceAdminClient->listInstanceConfigs( + Argument::that(function ($request) { + return $request->getParent() == InstanceAdminClient::projectName(self::PROJECT); + }), + Argument::type('array') + ) + ->shouldBeCalled() + ->willReturn($pagedListResponse->reveal()); - $configs = $this->client->instanceConfigurations(); + $configs = $this->spannerClient->instanceConfigurations(); $this->assertInstanceOf(ItemIterator::class, $configs); @@ -146,34 +168,60 @@ public function testInstanceConfigurations() */ public function testPagedInstanceConfigurations() { - $firstCall = [ - 'instanceConfigs' => [ - [ - 'name' => 'projects/foo/instanceConfigs/bar', - 'displayName' => 'Bar' - ] - ], - 'nextPageToken' => 'fooBar' - ]; - - $secondCall = [ - 'instanceConfigs' => [ - [ - 'name' => 'projects/foo/instanceConfigs/bat', - 'displayName' => 'Bat' + $page1 = $this->prophesize(Page::class); + $page1->getResponseObject() + ->willReturn(new ListInstanceConfigsResponse([ + 'instance_configs' => [ + new InstanceConfig([ + 'name' => 'projects/foo/instanceConfigs/bar', + 'display_name' => 'Bar' + ]) + ], + 'next_page_token' => 'fooBar' + ])); + + $pagedListResponse1 = $this->prophesize(PagedListResponse::class); + $pagedListResponse1->getPage() + ->willReturn($page1->reveal()); + + $page2 = $this->prophesize(Page::class); + $page2->getResponseObject() + ->willReturn(new ListInstanceConfigsResponse([ + 'instance_configs' => [ + new InstanceConfig([ + 'name' => 'projects/foo/instanceConfigs/bat', + 'display_name' => 'Bat' + ]) ] - ] - ]; - - $this->connection->listInstanceConfigs( - Argument::withEntry('projectName', InstanceAdminClient::projectName(self::PROJECT)) + ])); + + $pagedListResponse2 = $this->prophesize(PagedListResponse::class); + $pagedListResponse2->getPage() + ->willReturn($page2->reveal()); + + $iteration = 0; + $this->instanceAdminClient->listInstanceConfigs( + Argument::that(function ($request) use (&$iteration) { + $iteration++; + return $this->serializer->encodeMessage($request)['parent'] + == InstanceAdminClient::projectName(self::PROJECT) && $iteration == 1; + }), + Argument::type('array') ) - ->shouldBeCalledTimes(2) - ->willReturn($firstCall, $secondCall); - - $this->client->___setProperty('connection', $this->connection->reveal()); + ->shouldBeCalled() + ->willReturn($pagedListResponse1->reveal()); + + $this->instanceAdminClient->listInstanceConfigs( + Argument::that(function ($request) use (&$iteration) { + return $this->serializer->encodeMessage($request)['parent'] + == InstanceAdminClient::projectName(self::PROJECT) && $iteration == 2; + }), + Argument::type('array') + ) + ->shouldBeCalled() + ->willReturn($pagedListResponse2->reveal()); - $configs = $this->client->instanceConfigurations(); + $configs = $this->spannerClient->instanceConfigurations(); $this->assertInstanceOf(ItemIterator::class, $configs); @@ -188,7 +236,7 @@ public function testPagedInstanceConfigurations() */ public function testInstanceConfiguration() { - $config = $this->client->instanceConfiguration('bar'); + $config = $this->spannerClient->instanceConfiguration('bar'); $this->assertInstanceOf(InstanceConfiguration::class, $config); $this->assertEquals('bar', InstanceAdminClient::parseName($config->name())['instance_config']); @@ -199,24 +247,28 @@ public function testInstanceConfiguration() */ public function testCreateInstance() { - $this->connection->createInstance(Argument::that(function ($arg) { - if ($arg['name'] !== InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)) { - return false; - } - - return $arg['config'] === InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG); - })) + $this->instanceAdminClient->createInstance( + Argument::that(function ($request) use (&$iteration) { + $message = $this->serializer->encodeMessage($request); + $this->assertEquals( + $message['instance']['name'], + InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE) + ); + $this->assertEquals( + $message['instance']['config'], + InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG) + ); + return true; + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn([ - 'name' => 'operations/foo' - ]); - - $this->client->___setProperty('connection', $this->connection->reveal()); + ->willReturn($this->operationResponse->reveal()); $config = $this->prophesize(InstanceConfiguration::class); $config->name()->willReturn(InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG)); - $operation = $this->client->createInstance($config->reveal(), self::INSTANCE); + $operation = $this->spannerClient->createInstance($config->reveal(), self::INSTANCE); $this->assertInstanceOf(LongRunningOperation::class, $operation); } @@ -226,28 +278,31 @@ public function testCreateInstance() */ public function testCreateInstanceWithNodes() { - $this->connection->createInstance(Argument::that(function ($arg) { - if ($arg['name'] !== InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)) { - return false; - } - - if ($arg['config'] !== InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG)) { - return false; - } - - return isset($arg['nodeCount']) && $arg['nodeCount'] === 2; - })) + $this->instanceAdminClient->createInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + if ($message['instance']['name'] !== InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)) { + return false; + } + + if ($message['instance']['config'] !== InstanceAdminClient::instanceConfigName( + self::PROJECT, + self::CONFIG + )) { + return false; + } + + return isset($message['instance']['nodeCount']) && $message['instance']['nodeCount'] === 2; + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn([ - 'name' => 'operations/foo' - ]); - - $this->client->___setProperty('connection', $this->connection->reveal()); + ->willReturn($this->operationResponse->reveal()); $config = $this->prophesize(InstanceConfiguration::class); $config->name()->willReturn(InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG)); - $operation = $this->client->createInstance($config->reveal(), self::INSTANCE, [ + $operation = $this->spannerClient->createInstance($config->reveal(), self::INSTANCE, [ 'nodeCount' => 2 ]); @@ -259,28 +314,34 @@ public function testCreateInstanceWithNodes() */ public function testCreateInstanceWithProcessingUnits() { - $this->connection->createInstance(Argument::that(function ($arg) { - if ($arg['name'] !== InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)) { - return false; - } - - if ($arg['config'] !== InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG)) { - return false; - } - - return isset($arg['processingUnits']) && $arg['processingUnits'] === 2000; - })) + $this->instanceAdminClient->createInstance( + Argument::that(function ($request) { + $message = $this->serializer->encodeMessage($request); + if ($message['instance']['name'] !== InstanceAdminClient::instanceName( + self::PROJECT, + self::INSTANCE + )) { + return false; + } + if ($message['instance']['config'] !== InstanceAdminClient::instanceConfigName( + self::PROJECT, + self::CONFIG + )) { + return false; + } + + return isset($message['instance']['processingUnits']) + && $message['instance']['processingUnits'] === 2000; + }), + Argument::type('array') + ) ->shouldBeCalled() - ->willReturn([ - 'name' => 'operations/foo' - ]); - - $this->client->___setProperty('connection', $this->connection->reveal()); + ->willReturn($this->operationResponse->reveal()); $config = $this->prophesize(InstanceConfiguration::class); $config->name()->willReturn(InstanceAdminClient::instanceConfigName(self::PROJECT, self::CONFIG)); - $operation = $this->client->createInstance($config->reveal(), self::INSTANCE, [ + $operation = $this->spannerClient->createInstance($config->reveal(), self::INSTANCE, [ 'processingUnits' => 2000 ]); @@ -296,7 +357,7 @@ public function testCreateInstanceRaisesInvalidArgument() $config = $this->prophesize(InstanceConfiguration::class); - $this->client->createInstance($config->reveal(), self::INSTANCE, [ + $this->spannerClient->createInstance($config->reveal(), self::INSTANCE, [ 'nodeCount' => 2, 'processingUnits' => 2000, ]); @@ -307,7 +368,7 @@ public function testCreateInstanceRaisesInvalidArgument() */ public function testInstance() { - $i = $this->client->instance('foo'); + $i = $this->spannerClient->instance('foo'); $this->assertInstanceOf(Instance::class, $i); $this->assertEquals('foo', InstanceAdminClient::parseName($i->name())['instance']); } @@ -317,7 +378,7 @@ public function testInstance() */ public function testInstanceWithInstanceArray() { - $i = $this->client->instance('foo', ['key' => 'val']); + $i = $this->spannerClient->instance('foo', ['key' => 'val']); $this->assertEquals('val', $i->info()['key']); } @@ -326,20 +387,31 @@ public function testInstanceWithInstanceArray() */ public function testInstances() { - $this->connection->listInstances( - Argument::withEntry('projectName', InstanceAdminClient::projectName(self::PROJECT)) - ) - ->shouldBeCalled() - ->willReturn([ + $page = $this->prophesize(Page::class); + $page->getResponseObject() + ->willReturn(new ListInstancesResponse([ 'instances' => [ - ['name' => 'projects/test-project/instances/foo'], - ['name' => 'projects/test-project/instances/bar'], + new InstanceProto(['name' => 'projects/test-project/instances/foo']), + new InstanceProto(['name' => 'projects/test-project/instances/bar']), ] - ]); - - $this->client->___setProperty('connection', $this->connection->reveal()); + ])); + $pagedListResponse = $this->prophesize(PagedListResponse::class); + $pagedListResponse->getPage() + ->willReturn($page->reveal()); + $this->instanceAdminClient->listInstances( + Argument::that(function ($request) { + $this->assertEquals( + $request->getParent(), + InstanceAdminClient::projectName(self::PROJECT) + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalled() + ->willReturn($pagedListResponse->reveal()); - $instances = $this->client->instances(); + $instances = $this->spannerClient->instances(); $this->assertInstanceOf(ItemIterator::class, $instances); $instances = iterator_to_array($instances); @@ -348,115 +420,103 @@ public function testInstances() $this->assertEquals('bar', InstanceAdminClient::parseName($instances[1]->name())['instance']); } - /** - * @group spanner-admin - */ - 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); + $database = $this->spannerClient->connect(self::INSTANCE, self::DATABASE); $this->assertInstanceOf(Database::class, $database); $this->assertEquals(self::DATABASE, DatabaseAdminClient::parseName($database->name())['database']); } public function testConnectWithInstance() { - $inst = $this->client->instance(self::INSTANCE); - $database = $this->client->connect($inst, self::DATABASE); + $inst = $this->spannerClient->instance(self::INSTANCE); + $database = $this->spannerClient->connect($inst, self::DATABASE); $this->assertInstanceOf(Database::class, $database); $this->assertEquals(self::DATABASE, DatabaseAdminClient::parseName($database->name())['database']); } public function testKeyset() { - $ks = $this->client->keySet(); + $ks = $this->spannerClient->keySet(); $this->assertInstanceOf(KeySet::class, $ks); } public function testKeyRange() { - $kr = $this->client->keyRange(); + $kr = $this->spannerClient->keyRange(); $this->assertInstanceOf(KeyRange::class, $kr); } public function testBytes() { - $b = $this->client->bytes('foo'); + $b = $this->spannerClient->bytes('foo'); $this->assertInstanceOf(Bytes::class, $b); - $this->assertEquals(base64_encode('foo'), (string)$b); + $this->assertEquals(base64_encode('foo'), (string) $b); } public function testDate() { - $d = $this->client->date(new \DateTime); + $d = $this->spannerClient->date(new \DateTime()); $this->assertInstanceOf(Date::class, $d); } public function testTimestamp() { - $ts = $this->client->timestamp(new \DateTime); + $ts = $this->spannerClient->timestamp(new \DateTime()); $this->assertInstanceOf(Timestamp::class, $ts); } public function testNumeric() { - $n = $this->client->numeric('12345.123456789'); + $n = $this->spannerClient->numeric('12345.123456789'); $this->assertInstanceOf(Numeric::class, $n); } public function testPgNumeric() { - $decimalVal = $this->client->pgNumeric('12345.123456789'); + $decimalVal = $this->spannerClient->pgNumeric('12345.123456789'); $this->assertInstanceOf(PgNumeric::class, $decimalVal); - $scientificVal = $this->client->pgNumeric('1.09E100'); + $scientificVal = $this->spannerClient->pgNumeric('1.09E100'); $this->assertInstanceOf(PgNumeric::class, $scientificVal); } public function testPgJsonB() { - $strVal = $this->client->pgJsonb('{}'); + $strVal = $this->spannerClient->pgJsonb('{}'); $this->assertInstanceOf(PgJsonb::class, $strVal); - $arrVal = $this->client->pgJsonb(["a" => 1, "b" => 2]); + $arrVal = $this->spannerClient->pgJsonb(['a' => 1, 'b' => 2]); $this->assertInstanceOf(PgJsonb::class, $arrVal); $stub = $this->prophesize('stdClass'); $stub->willImplement('JsonSerializable'); - $stub->jsonSerialize()->willReturn(["a" => 1, "b" => null]); - $objVal = $this->client->pgJsonb($stub->reveal()); + $stub->jsonSerialize()->willReturn(['a' => 1, 'b' => null]); + $objVal = $this->spannerClient->pgJsonb($stub->reveal()); $this->assertInstanceOf(PgJsonb::class, $objVal); } public function testPgOid() { - $oidVal = $this->client->pgOid('123'); + $oidVal = $this->spannerClient->pgOid('123'); $this->assertInstanceOf(PgOid::class, $oidVal); } public function testInt64() { - $i64 = $this->client->int64('123'); + $i64 = $this->spannerClient->int64('123'); $this->assertInstanceOf(Int64::class, $i64); } public function testDuration() { - $d = $this->client->duration(10, 1); + $d = $this->spannerClient->duration(10, 1); $this->assertInstanceOf(Duration::class, $d); } public function testCommitTimestamp() { - $t = $this->client->commitTimestamp(); + $t = $this->spannerClient->commitTimestamp(); $this->assertInstanceOf(CommitTimestamp::class, $t); } @@ -464,12 +524,12 @@ public function testSpannerClientDatabaseRole() { $instance = $this->prophesize(Instance::class); $instance->database(Argument::any(), ['databaseRole' => 'Reader'])->shouldBeCalled(); - $this->client->connect($instance->reveal(), self::DATABASE, ['databaseRole' => 'Reader']); + $this->spannerClient->connect($instance->reveal(), self::DATABASE, ['databaseRole' => 'Reader']); } public function testSpannerClientWithDirectedRead() { - $instance = $this->client->instance('testInstance'); + $instance = $this->spannerClient->instance('testInstance'); $this->assertEquals( $instance->directedReadOptions(), $this->directedReadOptionsIncludeReplicas @@ -482,7 +542,8 @@ public function testClientPassesIsolationLevel() $client = new SpannerClient([ 'projectId' => self::PROJECT, 'directedReadOptions' => $this->directedReadOptionsIncludeReplicas, - 'isolationLevel' => IsolationLevel::REPEATABLE_READ + 'isolationLevel' => IsolationLevel::REPEATABLE_READ, + 'credentials' => Fixtures::KEYFILE_STUB_FIXTURE(), ]); $reflectedClient = new ReflectionClass($client); @@ -518,7 +579,8 @@ public function testTransactionHasCorrectIsolationLevel() $client = new SpannerClient([ 'projectId' => self::PROJECT, 'directedReadOptions' => $this->directedReadOptionsIncludeReplicas, - 'isolationLevel' => IsolationLevel::REPEATABLE_READ + 'isolationLevel' => IsolationLevel::REPEATABLE_READ, + 'credentials' => Fixtures::KEYFILE_STUB_FIXTURE(), ]); $reflectedClient = new ReflectionClass($client); diff --git a/Spanner/tests/Unit/StructTypeTest.php b/Spanner/tests/Unit/StructTypeTest.php index 465bbbda925e..d624e4db6679 100644 --- a/Spanner/tests/Unit/StructTypeTest.php +++ b/Spanner/tests/Unit/StructTypeTest.php @@ -54,7 +54,7 @@ public function testEnqueueInConstructor() public function testChainableAdd() { - $type = new StructType; + $type = new StructType(); $type->add($this->definition[0]['name'], $this->definition[0]['type']) ->add($this->definition[1]['name'], $this->definition[1]['child']); @@ -64,7 +64,7 @@ public function testChainableAdd() public function testAddUnnamed() { - $type = new StructType; + $type = new StructType(); $type->addUnnamed(Database::TYPE_STRING); $this->assertEquals($type->fields(), [ [ @@ -80,7 +80,7 @@ public function testAddInvalidType() $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Field type `foo` is not valid.'); - (new StructType)->add('name', 'foo'); + (new StructType())->add('name', 'foo'); } /** @@ -90,7 +90,7 @@ public function testInvalidTypeDefinition($type) { $this->expectException(InvalidArgumentException::class); - (new StructType)->add('foo', $type); + (new StructType())->add('foo', $type); } public function definitionTypes() @@ -103,8 +103,8 @@ public function definitionTypes() public function testAddChildStruct() { - $str = new StructType; - $str->add('foo', new StructType); + $str = new StructType(); + $str->add('foo', new StructType()); $fields = $str->fields(); $this->assertEquals(Database::TYPE_STRUCT, $fields[0]['type']); @@ -113,7 +113,7 @@ public function testAddChildStruct() public function testAddChildArray() { - $str = new StructType; + $str = new StructType(); $str->add('foo', new ArrayType(null)); $fields = $str->fields(); diff --git a/Spanner/tests/Unit/StructValueTest.php b/Spanner/tests/Unit/StructValueTest.php index ab79505030ab..3fa728252f0a 100644 --- a/Spanner/tests/Unit/StructValueTest.php +++ b/Spanner/tests/Unit/StructValueTest.php @@ -50,7 +50,7 @@ public function testConstructor() public function testAdd() { - $val = new StructValue; + $val = new StructValue(); $val->add($this->values[0]['name'], $this->values[0]['value']) ->add($this->values[1]['name'], $this->values[1]['value']); @@ -59,7 +59,7 @@ public function testAdd() public function testAddUnnamed() { - $val = new StructValue; + $val = new StructValue(); $val->addUnnamed($this->values[0]['value']) ->addUnnamed($this->values[1]['value']); diff --git a/Spanner/tests/Unit/TimestampTest.php b/Spanner/tests/Unit/TimestampTest.php index 10fbbb0a764c..083d6ebbbf1a 100644 --- a/Spanner/tests/Unit/TimestampTest.php +++ b/Spanner/tests/Unit/TimestampTest.php @@ -17,8 +17,8 @@ namespace Google\Cloud\Spanner\Tests\Unit; -use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Core\Testing\GrpcTestTrait; +use Google\Cloud\Spanner\Timestamp; use PHPUnit\Framework\TestCase; /** @@ -56,7 +56,7 @@ public function testCast() { $this->assertEquals( (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'), - (new \DateTime(str_replace('000000000', '000000', (string)$this->ts)))->format('U') + (new \DateTime(str_replace('000000000', '000000', (string) $this->ts)))->format('U') ); } diff --git a/Spanner/tests/Unit/TransactionConfigurationTraitTest.php b/Spanner/tests/Unit/TransactionConfigurationTraitTest.php index 527d42706c98..206a4e0db758 100644 --- a/Spanner/tests/Unit/TransactionConfigurationTraitTest.php +++ b/Spanner/tests/Unit/TransactionConfigurationTraitTest.php @@ -19,10 +19,10 @@ use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Core\TimeTrait; -use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\TransactionConfigurationTrait; +use Google\Protobuf\Duration; use PHPUnit\Framework\TestCase; /** @@ -46,9 +46,9 @@ public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->impl = new TransactionConfigurationTraitImplementation; - $this->duration = new Duration(10, 1); - $this->dur = ['seconds' => 10, 'nanos' => 1]; + $this->impl = new TransactionConfigurationTraitImplementation(); + $this->duration = new Duration(['seconds' => 10, 'nanos' => 1]); + $this->dur = new Duration(['seconds' => 10, 'nanos' => 1]); $this->directedReadOptionsIncludeReplicas = [ 'includeReplicas' => [ 'replicaSelections' => [ @@ -61,7 +61,7 @@ public function setUp(): void public function testTransactionSelectorBasicSnapshot() { $args = []; - $res = $this->impl->proxyTransactionSelector($args); + $res = $this->impl->transactionSelector($args); $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); $this->assertEquals($res[0]['singleUse']['readOnly'], ['strong' => true]); } @@ -69,7 +69,7 @@ public function testTransactionSelectorBasicSnapshot() public function testTransactionSelectorExistingId() { $args = ['transactionId' => self::TRANSACTION]; - $res = $this->impl->proxyTransactionSelector($args); + $res = $this->impl->transactionSelector($args); $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); $this->assertEquals(self::TRANSACTION, $res[0]['id']); } @@ -77,75 +77,75 @@ public function testTransactionSelectorExistingId() public function testTransactionSelectorReadWrite() { $args = ['transactionType' => SessionPoolInterface::CONTEXT_READWRITE]; - $res = $this->impl->proxyTransactionSelector($args); + $res = $this->impl->transactionSelector($args); $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $res[1]); - $this->assertEquals($this->impl->proxyConfigureTransactionOptions(), $res[0]['singleUse']); + $this->assertEquals($this->impl->configureReadWriteTransactionOptions([]), $res[0]['singleUse']); } public function testTransactionSelectorReadOnly() { $args = ['transactionType' => SessionPoolInterface::CONTEXT_READ]; - $res = $this->impl->proxyTransactionSelector($args); + $res = $this->impl->transactionSelector($args); $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); } public function testBegin() { $args = ['begin' => true]; - $res = $this->impl->proxyTransactionSelector($args); + $res = $this->impl->transactionSelector($args); $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); $this->assertEquals($res[0]['begin']['readOnly'], ['strong' => true]); } - public function testConfigureSnapshotOptionsReturnReadTimestamp() + public function testConfigureReadOnlyTransactionOptionsReturnReadTimestamp() { $args = ['returnReadTimestamp' => true]; - $res = $this->impl->proxyConfigureSnapshotOptions($args); + $res = $this->impl->configureReadOnlyTransactionOptions($args); $this->assertTrue($res['readOnly']['returnReadTimestamp']); } - public function testConfigureSnapshotOptionsStrong() + public function testConfigureReadOnlyTransactionOptionsStrong() { $args = ['strong' => true]; - $res = $this->impl->proxyConfigureSnapshotOptions($args); + $res = $this->impl->configureReadOnlyTransactionOptions($args); $this->assertTrue($res['readOnly']['strong']); } /** * @dataProvider timestamps */ - public function testConfigureSnapshotOptionsMinReadTimestamp($timestamp, $expected = null) + public function testConfigureReadOnlyTransactionOptionsMinReadTimestamp($timestamp, $expected = null) { $time = $this->parseTimeString($timestamp); $ts = new Timestamp($time[0], $time[1]); $args = ['minReadTimestamp' => $ts, 'singleUse' => true]; - $res = $this->impl->proxyConfigureSnapshotOptions($args); + $res = $this->impl->configureReadOnlyTransactionOptions($args); $this->assertEquals($expected ?: $timestamp, $res['readOnly']['minReadTimestamp']); } /** * @dataProvider timestamps */ - public function testConfigureSnapshotOptionsReadTimestamp($timestamp) + public function testConfigureReadOnlyTransactionOptionsReadTimestamp($timestamp) { $time = $this->parseTimeString($timestamp); $ts = new Timestamp($time[0], $time[1]); $args = ['readTimestamp' => $ts]; - $res = $this->impl->proxyConfigureSnapshotOptions($args); + $res = $this->impl->configureReadOnlyTransactionOptions($args); $this->assertEquals($timestamp, $res['readOnly']['readTimestamp']); } - public function testConfigureSnapshotOptionsMaxStaleness() + public function testConfigureReadOnlyTransactionOptionsMaxStaleness() { $args = ['maxStaleness' => $this->duration, 'singleUse' => true]; - $res = $this->impl->proxyConfigureSnapshotOptions($args); + $res = $this->impl->configureReadOnlyTransactionOptions($args); $this->assertEquals($this->dur, $res['readOnly']['maxStaleness']); } - public function testConfigureSnapshotOptionsExactStaleness() + public function testConfigureReadOnlyTransactionOptionsExactStaleness() { $args = ['exactStaleness' => $this->duration]; - $res = $this->impl->proxyConfigureSnapshotOptions($args); + $res = $this->impl->configureReadOnlyTransactionOptions($args); $this->assertEquals($this->dur, $res['readOnly']['exactStaleness']); } @@ -154,39 +154,39 @@ public function testTransactionSelectorInvalidContext() $this->expectException(\BadMethodCallException::class); $args = ['transactionType' => 'foo']; - $this->impl->proxyTransactionSelector($args); + $this->impl->transactionSelector($args); } - public function testConfigureSnapshotOptionsInvalidExactStaleness() + public function testConfigureReadOnlyTransactionOptionsInvalidExactStaleness() { $this->expectException(\BadMethodCallException::class); $args = ['exactStaleness' => 'foo']; - $this->impl->proxyConfigureSnapshotOptions($args); + $this->impl->configureReadOnlyTransactionOptions($args); } - public function testConfigureSnapshotOptionsInvalidMaxStaleness() + public function testConfigureReadOnlyTransactionOptionsInvalidMaxStaleness() { $this->expectException(\BadMethodCallException::class); $args = ['maxStaleness' => 'foo']; - $this->impl->proxyConfigureSnapshotOptions($args); + $this->impl->configureReadOnlyTransactionOptions($args); } - public function testConfigureSnapshotOptionsInvalidMinReadTimestamp() + public function testConfigureReadOnlyTransactionOptionsInvalidMinReadTimestamp() { $this->expectException(\BadMethodCallException::class); $args = ['minReadTimestamp' => 'foo']; - $this->impl->proxyConfigureSnapshotOptions($args); + $this->impl->configureReadOnlyTransactionOptions($args); } - public function testConfigureSnapshotOptionsInvalidReadTimestamp() + public function testConfigureReadOnlyTransactionOptionsInvalidReadTimestamp() { $this->expectException(\BadMethodCallException::class); $args = ['readTimestamp' => 'foo']; - $this->impl->proxyConfigureSnapshotOptions($args); + $this->impl->configureReadOnlyTransactionOptions($args); } public function testRequestLevelConfigureDirectedReadOptions() @@ -196,7 +196,7 @@ public function testRequestLevelConfigureDirectedReadOptions() 'directedReadOptions' => $this->directedReadOptionsIncludeReplicas ]; $clientOptions = []; - $res = $this->impl->proxyConfigureDirectedReadOptions($requestOptions, $clientOptions); + $res = $this->impl->configureDirectedReadOptions($requestOptions, $clientOptions); $this->assertEquals($res, $requestOptions['directedReadOptions']); } @@ -204,7 +204,7 @@ public function testClientLevelConfigureDirectedReadOptions() { $requestOptions = ['transaction' => ['singleUse' => true]]; $clientOptions = $this->directedReadOptionsIncludeReplicas; - $res = $this->impl->proxyConfigureDirectedReadOptions($requestOptions, $clientOptions); + $res = $this->impl->configureDirectedReadOptions($requestOptions, $clientOptions); $this->assertEquals($res, $clientOptions); } @@ -221,7 +221,7 @@ public function testPrioritizeRequestLevelConfigureDirectedReadOptions() ] ] ]; - $res = $this->impl->proxyConfigureDirectedReadOptions($requestOptions, $clientOptions); + $res = $this->impl->configureDirectedReadOptions($requestOptions, $clientOptions); $this->assertEquals($res, $requestOptions['directedReadOptions']); } @@ -237,26 +237,11 @@ public function timestamps() //@codingStandardsIgnoreStart 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); - } - - public function proxyConfigureDirectedReadOptions(array $args1, array $args2) - { - return $this->configureDirectedReadOptions($args1, $args2); + use TransactionConfigurationTrait { + transactionSelector as public; + configureReadWriteTransactionOptions as public; + configureReadOnlyTransactionOptions as public; + configureDirectedReadOptions as public; } } //@codingStandardsIgnoreEnd diff --git a/Spanner/tests/Unit/TransactionTest.php b/Spanner/tests/Unit/TransactionTest.php index 23e620889a31..3f643e34b313 100644 --- a/Spanner/tests/Unit/TransactionTest.php +++ b/Spanner/tests/Unit/TransactionTest.php @@ -18,22 +18,30 @@ namespace Google\Cloud\Spanner\Tests\Unit; use Google\ApiCore\ValidationException; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Core\TimeTrait; use Google\Cloud\Spanner\BatchDmlResult; use Google\Cloud\Spanner\Database; -use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; use Google\Cloud\Spanner\Result; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\Session; -use Google\Cloud\Spanner\Tests\OperationRefreshTrait; use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; -use Google\Cloud\Spanner\Tests\StubCreationTrait; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlRequest; +use Google\Cloud\Spanner\V1\ExecuteBatchDmlResponse; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\ReadRequest; +use Google\Cloud\Spanner\V1\ResultSet; +use Google\Cloud\Spanner\V1\ResultSetStats; +use Google\Cloud\Spanner\V1\RollbackRequest; use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; +use Google\Protobuf\Duration; +use Google\Rpc\Status; use InvalidArgumentException; use PHPUnit\Framework\TestCase; use Prophecy\Argument; @@ -45,11 +53,10 @@ class TransactionTest extends TestCase { use GrpcTestTrait; - use OperationRefreshTrait; use ProphecyTrait; use ResultGeneratorTrait; - use StubCreationTrait; use TimeTrait; + use ApiHelperTrait; const TIMESTAMP = '2017-01-09T18:05:22.534799Z'; @@ -61,173 +68,73 @@ class TransactionTest extends TestCase const TRANSACTION_TAG = 'my-transaction-tag'; const REQUEST_TAG = 'my-request-tag'; - private $connection; private $instance; private $session; private $database; private $operation; + private $serializer; + private $headers; + private $spannerClient; private $transaction; - private $singleUseTransaction; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->connection = $this->getConnStub(); - $this->operation = new Operation($this->connection->reveal(), false); + $this->serializer = new Serializer(); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->operation = new Operation( + $this->spannerClient->reveal(), + $this->serializer, + ); $this->session = new Session( - $this->connection->reveal(), + $this->spannerClient->reveal(), + $this->serializer, self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION ); - $args = [ + $this->transaction = new Transaction( $this->operation, $this->session, self::TRANSACTION, - false, - self::TRANSACTION_TAG - ]; - - $props = [ - 'operation', 'readTimestamp', 'state' - ]; - - $this->transaction = TestHelpers::stub(Transaction::class, $args, $props); - - $args = [ - $this->operation, - $this->session, - ]; - $this->singleUseTransaction = TestHelpers::stub(Transaction::class, $args, $props); + ['tag' => self::TRANSACTION_TAG] + ); } public function testSingleUseTagError() { $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot set a transaction tag on a single-use transaction.'); new Transaction( $this->operation, $this->session, null, - false, - self::TRANSACTION_TAG + ['tag' => self::TRANSACTION_TAG] ); } - public function testInsert() - { - $this->transaction->insert('Posts', ['foo' => 'bar']); - - $mutations = $this->transaction->___getProperty('mutationData'); - - $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('mutationData'); - - $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('mutationData'); - - $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('mutationData'); - - $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('mutationData'); - - $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('mutationData'); - - $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('mutationData'); - - $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('mutationData'); - - $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('mutationData'); - $this->assertEquals('Posts', $mutations[0]['delete']['table']); - $this->assertEquals('foo', $mutations[0]['delete']['keySet']['keys'][0]); - $this->assertArrayNotHasKey('all', $mutations[0]['delete']['keySet']); - } - public function testExecute() { $sql = 'SELECT * FROM Table'; - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('transaction', ['id' => self::TRANSACTION]), - Argument::withEntry('sql', $sql), - Argument::withEntry('headers', ['x-goog-spanner-route-to-leader' => ['true']]) - ))->shouldBeCalled()->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $this->assertEquals($request->getTransaction()->getId(), self::TRANSACTION); + $this->assertEquals($request->getSql(), $sql); + return true; + }), + Argument::that(function (array $callOptions) { + $this->assertEquals($callOptions['route-to-leader'], true); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $res = $this->transaction->execute($sql); $this->assertInstanceOf(Result::class, $res); @@ -238,19 +145,26 @@ public function testExecute() public function testExecuteUpdate() { $sql = 'UPDATE foo SET bar = @bar'; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', ['id' => self::TRANSACTION]), - Argument::withEntry('requestOptions', [ - 'requestTag' => self::REQUEST_TAG, - 'transactionTag' => self::TRANSACTION_TAG - ]), - Argument::withEntry('headers', ['x-goog-spanner-route-to-leader' => ['true']]) - ))->shouldBeCalled()->willReturn($this->resultGenerator(true)); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); - $res = $this->transaction->executeUpdate($sql, ['requestOptions' => ['requestTag' => self::REQUEST_TAG]]); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $this->assertEquals($request->getSql(), $sql); + $this->assertEquals($request->getTransaction()->getId(), self::TRANSACTION); + $this->assertEquals( + $request->getRequestOptions()->getRequestTag(), + self::REQUEST_TAG + ); + $this->assertEquals( + $request->getRequestOptions()->getTransactionTag(), + self::TRANSACTION_TAG + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream(null, new ResultSetStats(['row_count_exact' => 1]))); + $res = $this->transaction->executeUpdate($sql, ['requestOptions' => ['requestTag' => self::REQUEST_TAG]]); $this->assertEquals(1, $res); } @@ -270,109 +184,106 @@ public function testExecuteUpdateWithExcludeTxnFromChangeStreamsThrowsException( public function testDmlSeqno() { $sql = 'UPDATE foo SET bar = @bar'; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('seqno', 1), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - 'requestTag' => self::REQUEST_TAG - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator(true)); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); - $this->transaction->executeUpdate($sql, ['requestOptions' => ['requestTag' => self::REQUEST_TAG]]); - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('seqno', 2), - Argument::withEntry('requestOptions', [ - 'requestTag' => self::REQUEST_TAG, - 'transactionTag' => self::TRANSACTION_TAG - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator(true)); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); - $this->transaction->executeUpdate($sql, ['requestOptions' => ['requestTag' => self::REQUEST_TAG]]); - - $this->connection->executeBatchDml(Argument::allOf( - Argument::withEntry('seqno', 3), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - 'requestTag' => self::REQUEST_TAG - ]) - ))->shouldBeCalled()->willReturn([ - 'resultSets' => [] - ]); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) { + $this->assertEquals($request->getSeqno(), 1); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream(null, new ResultSetStats(['row_count_exact' => 1]))); + + $this->transaction->executeUpdate( + $sql, + ['requestOptions' => ['requestTag' => self::REQUEST_TAG]] + ); + + $this->spannerClient->executeBatchDml( + Argument::that(function (ExecuteBatchDmlRequest $request) { + $this->assertEquals( + $request->getSeqno(), + 2 + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new ExecuteBatchDmlResponse(['result_sets' => []])); - $this->refreshOperation($this->transaction, $this->connection->reveal()); $this->transaction->executeUpdateBatch( - [ - ['sql' => 'SELECT 1'], - ], + [['sql' => 'SELECT 1']], ['requestOptions' => ['requestTag' => self::REQUEST_TAG]] ); } public function testExecuteUpdateBatch() { - $this->connection->executeBatchDml(Argument::allOf( - Argument::withEntry('statements', [ - [ - 'sql' => 'SELECT 1', - 'params' => [], - 'paramTypes' => [] - ], [ - 'sql' => 'SELECT @foo', - 'params' => [ - 'foo' => 'bar' - ], - 'paramTypes' => [ - 'foo' => [ - 'code' => Database::TYPE_STRING - ] - ] - ], [ - 'sql' => 'SELECT @foo', - 'params' => [ - 'foo' => null - ], - 'paramTypes' => [ - 'foo' => [ - 'code' => Database::TYPE_STRING - ] - ] + $this->spannerClient->executeBatchDml( + Argument::that(function (ExecuteBatchDmlRequest $request) { + $this->assertEquals( + $request->getRequestOptions()->getRequestTag(), + self::REQUEST_TAG + ); + $this->assertEquals( + $request->getRequestOptions()->getTransactionTag(), + self::TRANSACTION_TAG + ); + $statements = $request->getStatements(); + $this->assertEquals(3, count($statements)); + + $statement1 = $statements[0]; + $this->assertEquals('SELECT 1', $statement1->getSql()); + $this->assertEmpty($statement1->getParams()); + $this->assertEmpty($statement1->getParamTypes()); + + $statement2 = $statements[1]; + $this->assertEquals('SELECT @foo', $statement2->getSql()); + $this->assertEquals('bar', $statement2->getParams()->getFields()['foo']->getStringValue()); + $types = $statement2->getParamTypes(); + $this->assertEquals(Database::TYPE_STRING, $types['foo']->getCode()); + + $statement3 = $statements[2]; + $this->assertEquals('SELECT @foo', $statement3->getSql()); + $this->assertEmpty($statement3->getParams()->getFields()['foo']->getStringValue()); + $types = $statement3->getParamTypes(); + $this->assertEquals(Database::TYPE_STRING, $types['foo']->getCode()); + return true; + }), + Argument::that(function (array $callOptions) { + $this->assertEquals($callOptions['route-to-leader'], true); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn(new ExecuteBatchDmlResponse([ + 'result_sets' => [ + new ResultSet([ + 'stats' => new ResultSetStats([ + 'row_count_exact' => 1 + ]) + ]), + new ResultSet([ + 'stats' => new ResultSetStats([ + 'row_count_exact' => 2 + ]) + ]), + new ResultSet([ + 'stats' => new ResultSetStats([ + 'row_count_exact' => 3 + ]) + ]) ] - ]), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - 'requestTag' => self::REQUEST_TAG - ]), - Argument::withEntry('headers', ['x-goog-spanner-route-to-leader' => ['true']]) - ))->shouldBeCalled()->willReturn([ - 'resultSets' => [ - [ - 'stats' => [ - 'rowCountExact' => 1 - ] - ], [ - 'stats' => [ - 'rowCountExact' => 2 - ] - ], [ - 'stats' => [ - 'rowCountExact' => 3 - ] - ] - ] - ]); + ])); - $this->refreshOperation($this->transaction, $this->connection->reveal()); $res = $this->transaction->executeUpdateBatch( $this->bdmlStatements(), ['requestOptions' => ['requestTag' => self::REQUEST_TAG]] ); - $this->assertInstanceOf(BatchDmlResult::class, $res); $this->assertNull($res->error()); - $this->assertEquals([1,2,3], $res->rowCounts()); + $this->assertEquals([1, 2, 3], $res->rowCounts()); } public function testExecuteUpdateBatchError() @@ -383,35 +294,45 @@ public function testExecuteUpdateBatchError() 'details' => [] ]; - $this->connection->executeBatchDml(Argument::allOf( - Argument::withEntry('session', $this->session->name()), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - 'requestTag' => self::REQUEST_TAG - ]) - ))->shouldBeCalled()->willReturn([ - 'resultSets' => [ - [ - 'stats' => [ - 'rowCountExact' => 1 - ] - ], [ - 'stats' => [ - 'rowCountExact' => 2 - ] - ] - ], - 'status' => $err - ]); + $this->spannerClient->executeBatchDml( + Argument::that(function (ExecuteBatchDmlRequest $request) { + $this->assertEquals($request->getSession(), $this->session->name()); + $this->assertEquals( + $request->getRequestOptions()->getRequestTag(), + self::REQUEST_TAG + ); + $this->assertEquals( + $request->getRequestOptions()->getTransactionTag(), + self::TRANSACTION_TAG + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new ExecuteBatchDmlResponse([ + 'result_sets' => [ + new ResultSet([ + 'stats' => new ResultSetStats([ + 'row_count_exact' => 1 + ]) + ]), + new ResultSet([ + 'stats' => new ResultSetStats([ + 'row_count_exact' => 2 + ]) + ]) + ], + 'status' => new Status($err) + ])); - $this->refreshOperation($this->transaction, $this->connection->reveal()); $statements = $this->bdmlStatements(); $res = $this->transaction->executeUpdateBatch( $statements, ['requestOptions' => ['requestTag' => self::REQUEST_TAG]] ); - $this->assertEquals([1,2], $res->rowCounts()); + $this->assertEquals([1, 2], $res->rowCounts()); $this->assertEquals($err, $res->error()['status']); $this->assertEquals($statements[2], $res->error()['statement']); } @@ -452,16 +373,25 @@ public function testExecuteUpdateNonDml() $this->expectException(InvalidArgumentException::class); $sql = 'UPDATE foo SET bar = @bar'; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', ['id' => self::TRANSACTION]), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - 'requestTag' => self::REQUEST_TAG - ]) - ))->shouldBeCalled()->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $this->assertEquals($request->getSql(), $sql); + $this->assertEquals($request->getTransaction()->getId(), self::TRANSACTION); + $this->assertEquals( + $request->getRequestOptions()->getRequestTag(), + self::REQUEST_TAG + ); + $this->assertEquals( + $request->getRequestOptions()->getTransactionTag(), + self::TRANSACTION_TAG + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); + $res = $this->transaction->executeUpdate($sql, ['requestOptions' => ['requestTag' => self::REQUEST_TAG]]); $this->assertEquals(1, $res); @@ -472,19 +402,32 @@ public function testRead() $table = 'Table'; $opts = ['foo' => 'bar']; - $this->connection->streamingRead(Argument::allOf( - Argument::withEntry('transaction', ['id' => self::TRANSACTION]), - Argument::withEntry('table', $table), - Argument::withEntry('keySet', ['all' => true]), - Argument::withEntry('columns', ['ID']), - Argument::withEntry('requestOptions', [ - 'transactionTag' => self::TRANSACTION_TAG, - 'requestTag' => self::REQUEST_TAG - ]), - Argument::withEntry('headers', ['x-goog-spanner-route-to-leader' => ['true']]) - ))->shouldBeCalled()->willReturn($this->resultGenerator()); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->streamingRead( + Argument::that(function (ReadRequest $request) use ($table) { + $this->assertEquals($request->getTransaction()->getId(), self::TRANSACTION); + $this->assertEquals($request->getTable(), $table); + $this->assertTrue($request->getKeySet()->getAll()); + $this->assertEquals(iterator_to_array($request->getColumns()), ['ID']); + $this->assertEquals( + $request->getRequestOptions()->getTransactionTag(), + self::TRANSACTION_TAG + ); + $this->assertEquals( + $request->getRequestOptions()->getRequestTag(), + self::REQUEST_TAG + ); + + return true; + }), + Argument::that(function (array $callOptions) { + $this->assertArrayHasKey('route-to-leader', $callOptions); + $this->assertEquals(true, $callOptions['route-to-leader']); + + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); $res = $this->transaction->read( $table, @@ -500,37 +443,49 @@ public function testRead() public function testCommit() { - $this->transaction->insert('Posts', ['foo' => 'bar']); - - $mutations = $this->transaction->___getProperty('mutationData'); - $operation = $this->prophesize(Operation::class); $operation->commitWithResponse( $this->session, - $mutations, + Argument::that(function ($mutations) { + $this->assertEquals(1, count($mutations)); + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + return true; + }), [ 'transactionId' => self::TRANSACTION, 'requestOptions' => [ 'transactionTag' => self::TRANSACTION_TAG ] ] - )->shouldBeCalled()->willReturn($this->commitResponseWithCommitStats()); + ) + ->shouldBeCalled() + ->willReturn($this->commitResponseWithCommitStats()); - $this->transaction->___setProperty('operation', $operation->reveal()); + $transaction = new Transaction( + $operation->reveal(), + $this->session, + self::TRANSACTION, + ['tag' => self::TRANSACTION_TAG] + ); - $this->transaction->commit(['requestOptions' => ['requestTag' => 'unused']]); + $transaction->insert('Posts', ['foo' => 'bar']); + $transaction->commit(['requestOptions' => ['requestTag' => 'unused']]); } public function testCommitWithReturnCommitStats() { - $this->transaction->insert('Posts', ['foo' => 'bar']); - - $mutations = $this->transaction->___getProperty('mutationData'); - $operation = $this->prophesize(Operation::class); $operation->commitWithResponse( $this->session, - $mutations, + Argument::that(function ($mutations) { + $this->assertEquals(1, count($mutations)); + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + return true; + }), [ 'transactionId' => self::TRANSACTION, 'returnCommitStats' => true, @@ -538,26 +493,38 @@ public function testCommitWithReturnCommitStats() 'transactionTag' => self::TRANSACTION_TAG ] ] - )->shouldBeCalled()->willReturn($this->commitResponseWithCommitStats()); + ) + ->shouldBeCalled() + ->willReturn($this->commitResponseWithCommitStats()); - $this->transaction->___setProperty('operation', $operation->reveal()); + $transaction = new Transaction( + $operation->reveal(), + $this->session, + self::TRANSACTION, + ['tag' => self::TRANSACTION_TAG] + ); - $this->transaction->commit(['returnCommitStats' => true]); + $transaction->insert('Posts', ['foo' => 'bar']); + $transaction->commit(['returnCommitStats' => true]); - $this->assertEquals(['mutationCount' => 1], $this->transaction->getCommitStats()); + $this->assertEquals(['mutationCount' => 1], $transaction->getCommitStats()); } public function testCommitWithMaxCommitDelay() { - $duration = new Duration(0, 100000000); - $this->transaction->insert('Posts', ['foo' => 'bar']); - - $mutations = $this->transaction->___getProperty('mutationData'); + $duration = new Duration(['seconds' => 0, 'nanos' => 100000000]); $operation = $this->prophesize(Operation::class); $operation->commitWithResponse( $this->session, - $mutations, + Argument::that(function ($mutations) { + $this->assertEquals(1, count($mutations)); + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + + return true; + }), [ 'transactionId' => self::TRANSACTION, 'returnCommitStats' => true, @@ -566,32 +533,58 @@ public function testCommitWithMaxCommitDelay() 'transactionTag' => self::TRANSACTION_TAG ] ] - )->shouldBeCalled()->willReturn($this->commitResponseWithCommitStats()); - - $this->transaction->___setProperty('operation', $operation->reveal()); + ) + ->shouldBeCalled() + ->willReturn($this->commitResponseWithCommitStats()); - $this->transaction->commit([ + $transaction = new Transaction( + $operation->reveal(), + $this->session, + self::TRANSACTION, + ['tag' => self::TRANSACTION_TAG] + ); + $transaction->insert('Posts', ['foo' => 'bar']); + $transaction->commit([ 'returnCommitStats' => true, 'maxCommitDelay' => $duration ]); - $this->assertEquals(['mutationCount' => 1], $this->transaction->getCommitStats()); + $this->assertEquals(['mutationCount' => 1], $transaction->getCommitStats()); } public function testCommitInvalidState() { $this->expectException(\BadMethodCallException::class); - $this->transaction->___setProperty('state', 'foo'); - $this->transaction->commit(); + $operation = $this->prophesize(Operation::class); + $operation->commitWithResponse(Argument::cetera()) + ->shouldBeCalledOnce() + ->willReturn([$this->prophesize(Timestamp::class)->reveal()]); + + $transaction = new Transaction( + $operation->reveal(), + $this->session, + self::TRANSACTION, + ['tag' => self::TRANSACTION_TAG] + ); + + // call "commit" to mock closing the state + $transaction->commit(); + + // transaction is considered closed after the first commit, so this should throw an exception + $transaction->commit(); } public function testRollback() { - $this->connection->rollback(Argument::withEntry('session', $this->session->name())) - ->shouldBeCalled(); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->rollback( + Argument::that(function (RollbackRequest $request) { + $this->assertEquals($request->getSession(), $this->session->name()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); $this->transaction->rollback(); } @@ -600,8 +593,23 @@ public function testRollbackInvalidState() { $this->expectException(\BadMethodCallException::class); - $this->transaction->___setProperty('state', 'foo'); - $this->transaction->rollback(); + $operation = $this->prophesize(Operation::class); + $operation->commitWithResponse(Argument::cetera()) + ->shouldBeCalledOnce() + ->willReturn([$this->prophesize(Timestamp::class)->reveal()]); + + $transaction = new Transaction( + $operation->reveal(), + $this->session, + self::TRANSACTION, + ['tag' => self::TRANSACTION_TAG] + ); + + // call "commit" to mock closing the state + $transaction->commit(); + + // transaction is considered closed after the first commit, so this should throw an exception + $transaction->rollback(); } public function testId() @@ -611,17 +619,35 @@ public function testId() public function testState() { - $this->assertEquals(Transaction::STATE_ACTIVE, $this->transaction->state()); + $operation = $this->prophesize(Operation::class); + $operation->commitWithResponse(Argument::cetera()) + ->shouldBeCalledOnce() + ->willReturn([$this->prophesize(Timestamp::class)->reveal()]); - $this->transaction->___setProperty('state', Transaction::STATE_COMMITTED); - $this->assertEquals(Transaction::STATE_COMMITTED, $this->transaction->state()); + $transaction = new Transaction( + $operation->reveal(), + $this->session, + self::TRANSACTION, + ['tag' => self::TRANSACTION_TAG] + ); + + $this->assertEquals(Transaction::STATE_ACTIVE, $transaction->state()); + + // call "commit" to mock closing the state + $transaction->commit(); + + $this->assertEquals(Transaction::STATE_COMMITTED, $transaction->state()); } public function testInvalidReadContext() { $this->expectException(\BadMethodCallException::class); - $this->singleUseTransaction->execute('foo'); + $singleUseTransaction = new Transaction( + $this->operation, + $this->session, + ); + $singleUseTransaction->execute('foo'); } public function testIsRetryFalse() @@ -631,14 +657,12 @@ public function testIsRetryFalse() public function testIsRetryTrue() { - $args = [ + $transaction = new Transaction( $this->operation, $this->session, self::TRANSACTION, - true - ]; - - $transaction = TestHelpers::stub(Transaction::class, $args); + ['isRetry' => true] + ); $this->assertTrue($transaction->isRetry()); } @@ -646,18 +670,22 @@ public function testIsRetryTrue() public function testExecuteUpdateWithIsolationLevel() { $sql = 'UPDATE foo SET bar = @bar'; - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('sql', $sql), - Argument::withEntry('transaction', [ - 'begin' => [ - 'readWrite' => [], - 'isolationLevel' => IsolationLevel::REPEATABLE_READ - ] - ]), - Argument::withEntry('headers', ['x-goog-spanner-route-to-leader' => ['true']]) - ))->shouldBeCalled()->willReturn($this->resultGenerator(true)); - - $this->refreshOperation($this->transaction, $this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $this->assertEquals($sql, $request->getSql()); + $this->assertEquals( + IsolationLevel::REPEATABLE_READ, + $request->getTransaction()->getBegin()->getIsolationLevel() + ); + $this->assertNotNull( + $request->getTransaction()->getBegin()->getReadWrite() + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalled() + ->willReturn($this->resultGeneratorStream(null, new ResultSetStats(['row_count_exact' => 1]))); // Transaction without an ID to be able to use `begin` $transaction = new Transaction( @@ -681,11 +709,13 @@ public function testExecuteUpdateWithIsolationLevel() public function testSingleUseWithIsolationLevelThrowsAnExceptionOnReadOnly() { $this->expectException(ValidationException::class); + $this->expectExceptionMessage( + 'The isolation level can only be applied to read/write transactions. ' + . 'Single use transactions are not read/write' + ); $sql = 'UPDATE foo SET bar = @bar'; - $this->refreshOperation($this->transaction, $this->connection->reveal()); - $options = [ 'transaction' => [ 'begin' => [ @@ -695,20 +725,16 @@ public function testSingleUseWithIsolationLevelThrowsAnExceptionOnReadOnly() ] ]; - $res = $this->singleUseTransaction->executeUpdate($sql, $options); - - $this->assertEquals(1, $res); + $singleUseTransaction = new Transaction( + $this->operation, + $this->session, + ); + $singleUseTransaction->executeUpdate($sql, $options); } - // ******* // Helpers - private function commitResponse() - { - return ['commitTimestamp' => self::TIMESTAMP]; - } - private function commitResponseWithCommitStats() { $time = $this->parseTimeString(self::TIMESTAMP); @@ -721,11 +747,4 @@ private function commitResponseWithCommitStats() ] ]; } - - 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/Spanner/tests/Unit/TransactionTypeTest.php b/Spanner/tests/Unit/TransactionTypeTest.php index 210d07a04d73..2109599777cc 100644 --- a/Spanner/tests/Unit/TransactionTypeTest.php +++ b/Spanner/tests/Unit/TransactionTypeTest.php @@ -17,24 +17,38 @@ namespace Google\Cloud\Spanner\Tests\Unit; -use Google\Cloud\Core\LongRunning\LongRunningConnectionInterface; +use Google\Cloud\Core\ApiHelperTrait; use Google\Cloud\Core\Testing\GrpcTestTrait; -use Google\Cloud\Core\Testing\TestHelpers; use Google\Cloud\Core\TimeTrait; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; -use Google\Cloud\Spanner\Connection\ConnectionInterface; +use Google\Cloud\Spanner\Admin\Database\V1\Client\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\Client\InstanceAdminClient; use Google\Cloud\Spanner\Database; -use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Operation; +use Google\Cloud\Spanner\Serializer; use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\Spanner\Snapshot; -use Google\Cloud\Spanner\Tests\StubCreationTrait; +use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; -use Google\Cloud\Spanner\V1\SpannerClient; -use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel; +use Google\Cloud\Spanner\V1\BeginTransactionRequest; +use Google\Cloud\Spanner\V1\Client\SpannerClient; +use Google\Cloud\Spanner\V1\CommitRequest; +use Google\Cloud\Spanner\V1\CommitResponse; +use Google\Cloud\Spanner\V1\CreateSessionRequest; +use Google\Cloud\Spanner\V1\DeleteSessionRequest; +use Google\Cloud\Spanner\V1\ExecuteSqlRequest; +use Google\Cloud\Spanner\V1\PartialResultSet; +use Google\Cloud\Spanner\V1\ReadRequest; +use Google\Cloud\Spanner\V1\RollbackRequest; +use Google\Cloud\Spanner\V1\Session; +use Google\Cloud\Spanner\V1\Transaction as TransactionProto; +use Google\Cloud\Spanner\V1\TransactionOptions; +use Google\Cloud\Spanner\V1\TransactionOptions\PBReadOnly; +use Google\Cloud\Spanner\V1\TransactionOptions\ReadWrite; +use Google\Protobuf\Duration; +use Google\Protobuf\Timestamp as TimestampProto; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -45,10 +59,10 @@ */ class TransactionTypeTest extends TestCase { + use ApiHelperTrait; use GrpcTestTrait; use ProphecyTrait; - use ResultTestTrait; - use StubCreationTrait; + use ResultGeneratorTrait; use TimeTrait; const PROJECT = 'my-project'; @@ -57,44 +71,80 @@ class TransactionTypeTest extends TestCase const TRANSACTION = 'my-transaction'; const SESSION = 'my-session'; - private $connection; - + private $spannerClient; + private $serializer; private $timestamp; + private $protoTimestamp; public function setUp(): void { $this->checkAndSkipGrpcTests(); - $this->timestamp = (new Timestamp(\DateTime::createFromFormat('U', time()), 500000005))->formatAsString(); + $time = \DateTime::createFromFormat('U', time()); + $nanos = 500000005; + $this->timestamp = new Timestamp($time, $nanos); + $this->protoTimestamp = new TimestampProto(['seconds' => $time->format('U'), 'nanos' => $nanos]); - $this->connection = $this->getConnStub(); + $this->spannerClient = $this->prophesize(SpannerClient::class); + $this->serializer = $this->prophesize(Serializer::class); - $this->connection->createSession( - Argument::withEntry('database', SpannerClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE)) + // mock serializer responses for sessions (used for streaming tests) + $this->serializer = $this->prophesize(Serializer::class); + $this->serializer->decodeMessage( + Argument::type(CreateSessionRequest::class), + Argument::type('array') + ) + ->willReturn(new CreateSessionRequest([ + 'database' => SpannerClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ])); + $this->serializer->encodeMessage(Argument::type(Session::class)) + ->willReturn(['name' => $this->getFullyQualifiedSessionName()]); + + $this->serializer->decodeMessage( + Argument::type(DeleteSessionRequest::class), + Argument::type('array') + ) + ->willReturn(new DeleteSessionRequest()); + + $this->spannerClient->createSession( + Argument::that(function (CreateSessionRequest $request) { + $this->assertEquals( + $request->getDatabase(), + SpannerClient::databaseName(self::PROJECT, self::INSTANCE, self::DATABASE) + ); + return true; + }), + Argument::type('array') ) - ->willReturn(['name' => SpannerClient::sessionName( - self::PROJECT, - self::INSTANCE, - self::DATABASE, - self::SESSION - )]); + ->willReturn(new Session(['name' => $this->getFullyQualifiedSessionName()])); + + $this->spannerClient->deleteSession(Argument::cetera()) + ->shouldBeCalledOnce(); } public function testDatabaseRunTransactionPreAllocate() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('singleUse', false), - Argument::withEntry('transactionOptions', [ - 'readWrite' => [], - 'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED - ]) - ))->shouldBeCalledTimes(1)->willReturn(['id' => self::TRANSACTION]); - - $this->connection->commit(Argument::withEntry('transactionId', self::TRANSACTION)) - ->shouldBeCalledTimes(1) - ->willReturn(['commitTimestamp' => $this->timestamp]); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertEquals($request->getSession(), $this->getFullyQualifiedSessionName()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); + + $this->spannerClient->commit( + Argument::that(function (CommitRequest $request) { + $this->assertEquals($request->getTransactionId(), self::TRANSACTION); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse(['commit_timestamp' => $this->protoTimestamp])); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $database->runTransaction(function ($t) { // Transaction gets created at the commit operation @@ -104,14 +154,22 @@ public function testDatabaseRunTransactionPreAllocate() public function testDatabaseRunTransactionSingleUse() { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->commit(Argument::withEntry('singleUseTransaction', ['readWrite' => []])) - ->shouldBeCalledTimes(1) - ->willReturn(['commitTimestamp' => $this->timestamp]); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + + $this->spannerClient->commit( + Argument::that(function (CommitRequest $request) { + $this->assertEquals( + $this->createTransactionOptions(), + $request->getSingleUseTransaction(), + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse(['commit_timestamp' => $this->protoTimestamp])); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $database->runTransaction(function ($t) { $this->assertNull($t->id()); @@ -122,16 +180,17 @@ public function testDatabaseRunTransactionSingleUse() public function testDatabaseTransactionPreAllocate() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('singleUse', false), - Argument::withEntry('transactionOptions', [ - 'readWrite' => [], - 'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED, - ]) - ))->shouldBeCalledTimes(1)->willReturn(['id' => self::TRANSACTION]); - - $database = $this->database($this->connection->reveal()); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertEquals($request->getOptions(), $this->createTransactionOptions()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); + $database = $this->database($this->spannerClient->reveal()); $transaction = $database->transaction(); $this->assertInstanceOf(Transaction::class, $transaction); @@ -140,10 +199,9 @@ public function testDatabaseTransactionPreAllocate() public function testDatabaseTransactionSingleUse() { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $transaction = $database->transaction(['singleUse' => true]); @@ -153,17 +211,22 @@ public function testDatabaseTransactionSingleUse() public function testDatabaseSnapshotPreAllocate() { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('singleUse', false), - Argument::withEntry('transactionOptions', [ - 'readOnly' => [ - 'strong' => true - ] - ]) - ))->shouldBeCalledTimes(1) - ->willReturn(['id' => self::TRANSACTION]); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertEquals( + $request->getOptions(), + $this->createTransactionOptions(['readOnly' => ['strong' => true]]) + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $database = $this->database($this->connection->reveal()); + $database = $database = $this->database( + $this->spannerClient->reveal(), + ); $snapshot = $database->snapshot(); @@ -173,10 +236,11 @@ public function testDatabaseSnapshotPreAllocate() public function testDatabaseSnapshotSingleUse() { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); - $database = $this->database($this->connection->reveal()); + $database = $database = $this->database( + $this->spannerClient->reveal(), + ); $snapshot = $database->snapshot(['singleUse' => true]); @@ -191,54 +255,44 @@ public function testDatabaseSingleUseSnapshotMinReadTimestampAndMaxStaleness($ch { $seconds = 1; $nanos = 2; + $duration = new Duration(['seconds' => $seconds, 'nanos' => $nanos]); + + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($duration) { + $readOnly = $request->getTransaction()?->getSingleUse()->getReadOnly(); + $this->assertNotNull($readOnly); + $this->assertNull($readOnly->getReadTimestamp()); + $this->assertEquals($duration, $readOnly->getMaxStaleness()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $time = $this->parseTimeString($this->timestamp); - $timestamp = new Timestamp($time[0], $time[1]); - $duration = new Duration($seconds, $nanos); - - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->executeStreamingSql(Argument::withEntry('transaction', [ - 'singleUse' => [ - 'readOnly' => [ - 'minReadTimestamp' => $this->timestamp, - 'maxStaleness' => [ - 'seconds' => $seconds, - 'nanos' => $nanos - ] - ] - ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); - - $database = $this->database($this->connection->reveal()); - + $database = $this->database($this->spannerClient->reveal()); $snapshot = $database->snapshot([ 'singleUse' => true, - 'minReadTimestamp' => $timestamp, + 'minReadTimestamp' => $this->timestamp, 'maxStaleness' => $duration ]); - $result = $snapshot->execute('SELECT * FROM Table')->rows()->current(); + $snapshot->execute('SELECT * FROM Table')->rows()->current(); } public function testDatabasePreAllocatedSnapshotMinReadTimestamp() { $this->expectException(\BadMethodCallException::class); - $time = $this->parseTimeString($this->timestamp); - $timestamp = new Timestamp($time[0], $time[1]); - - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->executeStreamingSql(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->deleteSession(Argument::cetera())->shouldNotBeCalled(); - $this->connection->executeStreamingSql(Argument::any()) - ->shouldNotbeCalled(); + $database = $this->database($this->spannerClient->reveal()); - $database = $this->database($this->connection->reveal()); - - $snapshot = $database->snapshot([ - 'minReadTimestamp' => $timestamp, + $database->snapshot([ + 'minReadTimestamp' => $this->timestamp, ]); } @@ -248,18 +302,15 @@ public function testDatabasePreAllocatedSnapshotMaxStaleness() $seconds = 1; $nanos = 2; + $duration = new Duration(['seconds' => $seconds, 'nanos' => $nanos]); - $duration = new Duration($seconds, $nanos); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->executeStreamingSql(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->deleteSession(Argument::cetera())->shouldNotBeCalled(); - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); + $database = $this->database($this->spannerClient->reveal()); - $this->connection->executeStreamingSql(Argument::any()) - ->shouldNotbeCalled(); - - $database = $this->database($this->connection->reveal()); - - $snapshot = $database->snapshot([ + $database->snapshot([ 'maxStaleness' => $duration ]); } @@ -271,33 +322,27 @@ public function testDatabaseSnapshotSingleUseReadTimestampAndExactStaleness($chu { $seconds = 1; $nanos = 2; + $duration = new Duration(['seconds' => $seconds, 'nanos' => $nanos]); + + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($duration) { + $readOnly = $request->getTransaction()?->getSingleUse()?->getReadOnly(); + $this->assertNotNull($readOnly); + $this->assertNull($readOnly->getReadTimestamp()); + $this->assertEquals($duration, $readOnly->getExactStaleness()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $time = $this->parseTimeString($this->timestamp); - $timestamp = new Timestamp($time[0], $time[1]); - $duration = new Duration($seconds, $nanos); - - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->executeStreamingSql(Argument::allOf( - Argument::withEntry('transaction', [ - 'singleUse' => [ - 'readOnly' => [ - 'readTimestamp' => $this->timestamp, - 'exactStaleness' => [ - 'seconds' => $seconds, - 'nanos' => $nanos - ] - ] - ] - ]) - ))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); - - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $snapshot = $database->snapshot([ 'singleUse' => true, - 'readTimestamp' => $timestamp, + 'readTimestamp' => $this->timestamp, 'exactStaleness' => $duration ]); @@ -312,33 +357,31 @@ public function testDatabaseSnapshotPreAllocateReadTimestampAndExactStaleness($c $seconds = 1; $nanos = 2; - $time = $this->parseTimeString($this->timestamp); - $timestamp = new Timestamp($time[0], $time[1]); - $duration = new Duration($seconds, $nanos); - - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('singleUse', false), - Argument::withEntry('transactionOptions', [ - 'readOnly' => [ - 'readTimestamp' => $this->timestamp, - 'exactStaleness' => [ - 'seconds' => $seconds, - 'nanos' => $nanos - ] - ] - ]) - ))->shouldBeCalledTimes(1)->willReturn([ - 'id' => self::TRANSACTION - ]); - - $this->connection->executeStreamingSql(Argument::withEntry('transaction', ['id' => self::TRANSACTION])) - ->shouldBeCalledTimes(1) - ->willReturn($this->resultGenerator($chunks)); + $duration = new Duration(['seconds' => $seconds, 'nanos' => $nanos]); + $transaction = new TransactionProto(['id' => self::TRANSACTION]); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) use ($duration) { + $readOnly = $request->getOptions()?->getReadOnly(); + $this->assertNotNull($readOnly); + $this->assertNull($readOnly->getReadTimestamp()); + $this->assertEquals($duration, $readOnly->getExactStaleness()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($transaction); - $database = $this->database($this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); + $database = $this->database($this->spannerClient->reveal()); $snapshot = $database->snapshot([ - 'readTimestamp' => $timestamp, + 'readTimestamp' => $this->timestamp, 'exactStaleness' => $duration ]); @@ -350,18 +393,18 @@ public function testDatabaseSnapshotPreAllocateReadTimestampAndExactStaleness($c */ public function testDatabaseSingleUseSnapshotStrongConsistency($chunks) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->executeStreamingSql(Argument::withEntry('transaction', [ - 'singleUse' => [ - 'readOnly' => [ - 'strong' => true - ] - ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) { + $this->assertTrue($request->getTransaction()->getSingleUse()->getReadOnly()->getStrong()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $snapshot = $database->snapshot([ 'singleUse' => true, @@ -376,22 +419,25 @@ public function testDatabaseSingleUseSnapshotStrongConsistency($chunks) */ public function testDatabasePreAllocatedSnapshotStrongConsistency($chunks) { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('singleUse', false), - Argument::withEntry('transactionOptions', [ - 'readOnly' => [ - 'strong' => true - ] - ]) - ))->shouldBeCalledTimes(1)->willReturn([ - 'id' => self::TRANSACTION - ]); + $transaction = new TransactionProto(['id' => self::TRANSACTION]); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertTrue($request->getOptions()->getReadOnly()->getStrong()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($transaction); - $this->connection->executeStreamingSql(Argument::withEntry('transaction', ['id' => self::TRANSACTION])) - ->shouldBeCalledTimes(1) - ->willReturn($this->resultGenerator($chunks)); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $snapshot = $database->snapshot([ 'strong' => true @@ -405,18 +451,18 @@ public function testDatabasePreAllocatedSnapshotStrongConsistency($chunks) */ public function testDatabaseSingleUseSnapshotDefaultsToStrongConsistency($chunks) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->executeStreamingSql(Argument::withEntry('transaction', [ - 'singleUse' => [ - 'readOnly' => [ - 'strong' => true - ] - ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) { + $this->assertTrue($request->getTransaction()->getSingleUse()->getReadOnly()->getStrong()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $snapshot = $database->snapshot([ 'singleUse' => true, @@ -430,22 +476,25 @@ public function testDatabaseSingleUseSnapshotDefaultsToStrongConsistency($chunks */ public function testDatabasePreAllocatedSnapshotDefaultsToStrongConsistency($chunks) { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('singleUse', false), - Argument::withEntry('transactionOptions', [ - 'readOnly' => [ - 'strong' => true - ] - ]) - ))->shouldBeCalledTimes(1)->willReturn([ - 'id' => self::TRANSACTION - ]); + $transaction = new TransactionProto(['id' => self::TRANSACTION]); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertTrue($request->getOptions()->getReadOnly()->getStrong()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($transaction); - $this->connection->executeStreamingSql(Argument::withEntry('transaction', ['id' => self::TRANSACTION])) - ->shouldBeCalledTimes(1) - ->willReturn($this->resultGenerator($chunks)); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $snapshot = $database->snapshot(); @@ -457,22 +506,25 @@ public function testDatabasePreAllocatedSnapshotDefaultsToStrongConsistency($chu */ public function testDatabaseSnapshotReturnReadTimestamp($chunks) { - $this->connection->beginTransaction(Argument::allOf( - Argument::withEntry('singleUse', false), - Argument::withEntry('transactionOptions', [ - 'readOnly' => [ - 'returnReadTimestamp' => true - ] - ]) - ))->shouldBeCalledTimes(1)->willReturn([ - 'id' => self::TRANSACTION - ]); + $transaction = new TransactionProto(['id' => self::TRANSACTION]); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertTrue($request->getOptions()->getReadOnly()->getReturnReadTimestamp()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($transaction); - $this->connection->executeStreamingSql(Argument::withEntry('transaction', ['id' => self::TRANSACTION])) - ->shouldBeCalledTimes(1) - ->willReturn($this->resultGenerator($chunks)); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $snapshot = $database->snapshot([ 'returnReadTimestamp' => true @@ -483,13 +535,20 @@ public function testDatabaseSnapshotReturnReadTimestamp($chunks) public function testDatabaseInsertSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); + $this->spannerClient->commit( + Argument::that(function (CommitRequest $request) { + $this->assertEquals( + $request->getSingleUseTransaction(), + $this->createTransactionOptions() + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse(['commit_timestamp' => $this->protoTimestamp])); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $database->insert('Table', [ 'column' => 'value' @@ -498,13 +557,7 @@ public function testDatabaseInsertSingleUseReadWrite() public function testDatabaseInsertBatchSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); - - $database = $this->database($this->connection->reveal()); + $database = $this->createMockedCommitDatabase(); $database->insertBatch('Table', [[ 'column' => 'value' @@ -513,13 +566,7 @@ public function testDatabaseInsertBatchSingleUseReadWrite() public function testDatabaseUpdateSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); - - $database = $this->database($this->connection->reveal()); + $database = $this->createMockedCommitDatabase(); $database->update('Table', [ 'column' => 'value' @@ -528,13 +575,7 @@ public function testDatabaseUpdateSingleUseReadWrite() public function testDatabaseUpdateBatchSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); - - $database = $this->database($this->connection->reveal()); + $database = $this->createMockedCommitDatabase(); $database->updateBatch('Table', [[ 'column' => 'value' @@ -543,13 +584,7 @@ public function testDatabaseUpdateBatchSingleUseReadWrite() public function testDatabaseInsertOrUpdateSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); - - $database = $this->database($this->connection->reveal()); + $database = $this->createMockedCommitDatabase(); $database->insertOrUpdate('Table', [ 'column' => 'value' @@ -558,13 +593,7 @@ public function testDatabaseInsertOrUpdateSingleUseReadWrite() public function testDatabaseInsertOrUpdateBatchSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); - - $database = $this->database($this->connection->reveal()); + $database = $this->createMockedCommitDatabase(); $database->insertOrUpdateBatch('Table', [[ 'column' => 'value' @@ -573,13 +602,7 @@ public function testDatabaseInsertOrUpdateBatchSingleUseReadWrite() public function testDatabaseReplaceSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); - - $database = $this->database($this->connection->reveal()); + $database = $this->createMockedCommitDatabase(); $database->replace('Table', [ 'column' => 'value' @@ -588,13 +611,7 @@ public function testDatabaseReplaceSingleUseReadWrite() public function testDatabaseReplaceBatchSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); - - $database = $this->database($this->connection->reveal()); + $database = $this->createMockedCommitDatabase(); $database->replaceBatch('Table', [[ 'column' => 'value' @@ -603,15 +620,9 @@ public function testDatabaseReplaceBatchSingleUseReadWrite() public function testDatabaseDeleteSingleUseReadWrite() { - $this->connection->commit(Argument::withEntry('singleUseTransaction', [ - 'readWrite' => [] - ]))->shouldBeCalled()->willReturn([ - 'commitTimestamp' => $this->timestamp - ]); - - $database = $this->database($this->connection->reveal()); + $database = $this->createMockedCommitDatabase(); - $database->delete('Table', new KeySet); + $database->delete('Table', new KeySet()); } /** @@ -619,18 +630,23 @@ public function testDatabaseDeleteSingleUseReadWrite() */ public function testDatabaseExecuteSingleUseReadOnly($chunks) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->executeStreamingSql(Argument::withEntry('transaction', [ + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $transaction = [ 'singleUse' => [ 'readOnly' => [ 'strong' => true ] ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + ]; + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); + $serializer = $this->serializerForStreamingSql($chunks, $transaction); + $database = $this->database($this->spannerClient->reveal(), $serializer); $database->execute('SELECT * FROM Table')->rows()->current(); } @@ -639,18 +655,25 @@ public function testDatabaseExecuteSingleUseReadOnly($chunks) */ public function testDatabaseExecuteBeginReadOnly($chunks) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); - $this->connection->executeStreamingSql(Argument::withEntry('transaction', [ + $transaction = [ 'begin' => [ 'readOnly' => [ 'strong' => true ] ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + ]; - $database = $this->database($this->connection->reveal()); + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); + + $serializer = $this->serializerForStreamingSql($chunks, $transaction); + $database = $this->database($this->spannerClient->reveal(), $serializer); $database->execute('SELECT * FROM Table', [ 'begin' => true ])->rows()->current(); @@ -661,16 +684,21 @@ public function testDatabaseExecuteBeginReadOnly($chunks) */ public function testDatabaseExecuteBeginReadWrite($chunks) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->executeStreamingSql(Argument::withEntry('transaction', [ + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $transaction = [ 'begin' => [ 'readWrite' => [] ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + ]; + $this->spannerClient->executeStreamingSql( + Argument::type(ExecuteSqlRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); + $serializer = $this->serializerForStreamingSql($chunks, $transaction); + $database = $this->database($this->spannerClient->reveal(), $serializer); $database->execute('SELECT * FROM Table', [ 'begin' => true, 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE @@ -682,19 +710,24 @@ public function testDatabaseExecuteBeginReadWrite($chunks) */ public function testDatabaseReadSingleUseReadOnly($chunks) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->streamingRead(Argument::withEntry('transaction', [ + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $transaction = [ 'singleUse' => [ 'readOnly' => [ 'strong' => true ] ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + ]; + $this->spannerClient->streamingRead( + Argument::type(ReadRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); - $database->read('Table', new KeySet, [])->rows()->current(); + $serializer = $this->serializerForStreamingRead($chunks, $transaction); + $database = $this->database($this->spannerClient->reveal(), $serializer); + $database->read('Table', new KeySet(), [])->rows()->current(); } /** @@ -702,19 +735,25 @@ public function testDatabaseReadSingleUseReadOnly($chunks) */ public function testDatabaseReadBeginReadOnly($chunks) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->streamingRead(Argument::withEntry('transaction', [ + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $transaction = [ 'begin' => [ 'readOnly' => [ 'strong' => true ] ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + ]; + + $this->spannerClient->streamingRead( + Argument::type(ReadRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); - $database->read('Table', new KeySet, [], [ + $serializer = $this->serializerForStreamingRead($chunks, $transaction); + $database = $this->database($this->spannerClient->reveal(), $serializer); + $database->read('Table', new KeySet(), [], [ 'begin' => true ])->rows()->current(); } @@ -724,17 +763,22 @@ public function testDatabaseReadBeginReadOnly($chunks) */ public function testDatabaseReadBeginReadWrite($chunks) { - $this->connection->beginTransaction(Argument::any()) - ->shouldNotbeCalled(); - - $this->connection->streamingRead(Argument::withEntry('transaction', [ + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $transaction = [ 'begin' => [ 'readWrite' => [] ] - ]))->shouldBeCalledTimes(1)->willReturn($this->resultGenerator($chunks)); + ]; + $this->spannerClient->streamingRead( + Argument::type(ReadRequest::class), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream($chunks)); - $database = $this->database($this->connection->reveal()); - $database->read('Table', new KeySet, [], [ + $serializer = $this->serializerForStreamingRead($chunks, $transaction); + $database = $this->database($this->spannerClient->reveal(), $serializer); + $database->read('Table', new KeySet(), [], [ 'begin' => true, 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE ])->rows()->current(); @@ -742,24 +786,34 @@ public function testDatabaseReadBeginReadWrite($chunks) public function testTransactionPreAllocatedRollback() { - $this->connection->beginTransaction(Argument::withEntry('transactionOptions', [ - 'readWrite' => [], - 'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED - ]))->shouldBeCalledTimes(1)->willReturn(['id' => self::TRANSACTION]); + $this->spannerClient->beginTransaction( + Argument::that(function (BeginTransactionRequest $request) { + $this->assertEquals($request->getOptions(), $this->createTransactionOptions()); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new TransactionProto(['id' => self::TRANSACTION])); - $sess = SpannerClient::sessionName( + $session = SpannerClient::sessionName( self::PROJECT, self::INSTANCE, self::DATABASE, self::SESSION ); + $this->spannerClient->rollback( + Argument::that(function ($request) use ($session) { + Argument::type(RollbackRequest::class); + $this->assertEquals($request->getTransactionId(), self::TRANSACTION); + $this->assertEquals($request->getSession(), $session); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce(); - $this->connection->rollback(Argument::allOf( - Argument::withEntry('transactionId', self::TRANSACTION), - Argument::withEntry('session', $sess) - ))->shouldBeCalled(); - - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $t = $database->transaction(); $t->rollback(); } @@ -768,32 +822,131 @@ public function testTransactionSingleUseRollback() { $this->expectException(\BadMethodCallException::class); - $this->connection->beginTransaction(Argument::any())->shouldNotbeCalled(); - $this->connection->rollback(Argument::any())->shouldNotbeCalled(); + $this->spannerClient->beginTransaction(Argument::cetera())->shouldNotBeCalled(); + $this->spannerClient->rollback(Argument::cetera())->shouldNotBeCalled(); - $database = $this->database($this->connection->reveal()); + $database = $this->database($this->spannerClient->reveal()); $t = $database->transaction(['singleUse' => true]); $t->rollback(); } - private function database(ConnectionInterface $connection) + private function database(SpannerClient $spannerClient, ?Serializer $serializer = null) { - $operation = new Operation($connection, false); $instance = $this->prophesize(Instance::class); $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); $instance->directedReadOptions()->willReturn([]); - $database = TestHelpers::stub(Database::class, [ - $connection, + $database = new Database( + $spannerClient, + $this->prophesize(DatabaseAdminClient::class)->reveal(), + $serializer ?: new Serializer(), $instance->reveal(), - $this->prophesize(LongRunningConnectionInterface::class)->reveal(), - [], self::PROJECT, self::DATABASE - ], ['operation']); - - $database->___setProperty('operation', $operation); + ); return $database; } + + private function serializerForStreamingRead(array $chunks, array $expectedTransaction): Serializer + { + // mock serializer responses for streaming read + $this->serializer->decodeMessage( + Argument::type(ReadRequest::class), + Argument::that(function ($data) use ($expectedTransaction) { + $this->assertEquals($data['transaction'], $expectedTransaction); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn(new ReadRequest()); + + foreach ($chunks as $chunk) { + $result = new PartialResultSet(); + $result->mergeFromJsonString($chunk); + $this->serializer->encodeMessage($result) + ->shouldBeCalledOnce() + ->willReturn(json_decode($chunk, true)); + } + + return $this->serializer->reveal(); + } + + private function serializerForStreamingSql(array $chunks, array $expectedTransaction): Serializer + { + // mock serializer responses for streaming read + $this->serializer->decodeMessage( + Argument::type(ExecuteSqlRequest::class), + Argument::that(function ($data) use ($expectedTransaction) { + $this->assertEquals($expectedTransaction, $data['transaction']); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn(new ExecuteSqlRequest()); + + $this->serializer->decodeMessage( + Argument::type(BeginTransactionRequest::class), + Argument::type('array') + ) + ->willReturn(new BeginTransactionRequest()); + + foreach ($chunks as $chunk) { + $result = new PartialResultSet(); + $result->mergeFromJsonString($chunk); + $this->serializer->encodeMessage($result) + ->shouldBeCalledOnce() + ->willReturn(json_decode($chunk, true)); + } + + return $this->serializer->reveal(); + } + + private function getFullyQualifiedSessionName() + { + return SpannerClient::sessionName( + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + ); + } + + private function createTransactionOptions($options = []) + { + $serializer = new Serializer(); + $transactionOptions = new TransactionOptions(); + if (isset($options['readOnly'])) { + $readOnly = $serializer->decodeMessage( + new PBReadOnly(), + $options['readOnly'] + ); + $transactionOptions->setReadOnly($readOnly); + } else { + $readWrite = $readOnly = $serializer->decodeMessage( + new ReadWrite(), + $options['readOnly'] ?? [] + ); + $transactionOptions->setReadWrite($readWrite); + } + return $transactionOptions; + } + + private function createMockedCommitDatabase() + { + $this->spannerClient->commit( + Argument::that(function (CommitRequest $request) { + $this->assertEquals( + $request->getSingleUseTransaction(), + $this->createTransactionOptions() + ); + return true; + }), + Argument::type('array') + ) + ->shouldBeCalledOnce() + ->willReturn(new CommitResponse(['commit_timestamp' => $this->protoTimestamp])); + + return $this->database($this->spannerClient->reveal()); + } } diff --git a/Spanner/tests/Unit/V1/SpannerClientPartialVeneerTest.php b/Spanner/tests/Unit/V1/SpannerClientPartialVeneerTest.php deleted file mode 100644 index ff77aaddce50..000000000000 --- a/Spanner/tests/Unit/V1/SpannerClientPartialVeneerTest.php +++ /dev/null @@ -1,45 +0,0 @@ -<?php -/** - * Copyright 2018 Google LLC - * - * 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. - */ - -namespace Google\Cloud\Spanner\Tests\Unit\V1; - -use Google\ApiCore\CredentialsWrapper; -use Google\ApiCore\Testing\MockTransport; -use Google\Cloud\Spanner\V1\SpannerClient; -use PHPUnit\Framework\TestCase; - -/** - * @group spanner - * @group gapic - */ -class SpannerClientPartialVeneerTest extends TestCase -{ - public function testGetTransport() - { - $options = [ - 'credentials' => $this->getMockBuilder(CredentialsWrapper::class) - ->disableOriginalConstructor() - ->getMock(), - ]; - - $client = new SpannerClient($options + [ - 'transport' => new MockTransport(null) - ]); - - $this->assertInstanceOf(MockTransport::class, $client->getTransport()); - } -} diff --git a/Spanner/tests/Unit/V1/SpannerClientTest.php b/Spanner/tests/Unit/V1/SpannerClientTest.php deleted file mode 100644 index 6f7a869b0945..000000000000 --- a/Spanner/tests/Unit/V1/SpannerClientTest.php +++ /dev/null @@ -1,1173 +0,0 @@ -<?php -/* - * Copyright 2018 Google LLC - * - * 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 - * - * https://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. - */ - -/* - * GENERATED CODE WARNING - * This file was automatically generated - do not edit! - */ - -namespace Google\Cloud\Spanner\Tests\Unit\V1; - -use Google\ApiCore\ApiException; -use Google\ApiCore\CredentialsWrapper; -use Google\ApiCore\ServerStream; -use Google\ApiCore\Testing\GeneratedTest; -use Google\ApiCore\Testing\MockTransport; -use Google\Cloud\Spanner\V1\BatchCreateSessionsResponse; -use Google\Cloud\Spanner\V1\BatchWriteResponse; -use Google\Cloud\Spanner\V1\CommitResponse; -use Google\Cloud\Spanner\V1\ExecuteBatchDmlResponse; -use Google\Cloud\Spanner\V1\KeySet; -use Google\Cloud\Spanner\V1\ListSessionsResponse; -use Google\Cloud\Spanner\V1\PartialResultSet; -use Google\Cloud\Spanner\V1\PartitionResponse; -use Google\Cloud\Spanner\V1\ResultSet; -use Google\Cloud\Spanner\V1\Session; -use Google\Cloud\Spanner\V1\SpannerClient; -use Google\Cloud\Spanner\V1\Transaction; -use Google\Cloud\Spanner\V1\TransactionOptions; -use Google\Cloud\Spanner\V1\TransactionSelector; -use Google\Protobuf\GPBEmpty; -use Google\Rpc\Code; -use stdClass; - -/** - * @group spanner - * - * @group gapic - */ -class SpannerClientTest extends GeneratedTest -{ - /** @return TransportInterface */ - private function createTransport($deserialize = null) - { - return new MockTransport($deserialize); - } - - /** @return CredentialsWrapper */ - private function createCredentials() - { - return $this->getMockBuilder(CredentialsWrapper::class)->disableOriginalConstructor()->getMock(); - } - - /** @return SpannerClient */ - private function createClient(array $options = []) - { - $options += [ - 'credentials' => $this->createCredentials(), - ]; - return new SpannerClient($options); - } - - /** @test */ - public function batchCreateSessionsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new BatchCreateSessionsResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $sessionCount = 185691686; - $response = $gapicClient->batchCreateSessions($formattedDatabase, $sessionCount); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/BatchCreateSessions', $actualFuncCall); - $actualValue = $actualRequestObject->getDatabase(); - $this->assertProtobufEquals($formattedDatabase, $actualValue); - $actualValue = $actualRequestObject->getSessionCount(); - $this->assertProtobufEquals($sessionCount, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function batchCreateSessionsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $sessionCount = 185691686; - try { - $gapicClient->batchCreateSessions($formattedDatabase, $sessionCount); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function batchWriteTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new BatchWriteResponse(); - $transport->addResponse($expectedResponse); - $expectedResponse2 = new BatchWriteResponse(); - $transport->addResponse($expectedResponse2); - $expectedResponse3 = new BatchWriteResponse(); - $transport->addResponse($expectedResponse3); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $mutationGroups = []; - $serverStream = $gapicClient->batchWrite($formattedSession, $mutationGroups); - $this->assertInstanceOf(ServerStream::class, $serverStream); - $responses = iterator_to_array($serverStream->readAll()); - $expectedResponses = []; - $expectedResponses[] = $expectedResponse; - $expectedResponses[] = $expectedResponse2; - $expectedResponses[] = $expectedResponse3; - $this->assertEquals($expectedResponses, $responses); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/BatchWrite', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getMutationGroups(); - $this->assertProtobufEquals($mutationGroups, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function batchWriteExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->setStreamingStatus($status); - $this->assertTrue($transport->isExhausted()); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $mutationGroups = []; - $serverStream = $gapicClient->batchWrite($formattedSession, $mutationGroups); - $results = $serverStream->readAll(); - try { - iterator_to_array($results); - // If the close stream method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function beginTransactionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $id = '27'; - $expectedResponse = new Transaction(); - $expectedResponse->setId($id); - $transport->addResponse($expectedResponse); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $options = new TransactionOptions(); - $response = $gapicClient->beginTransaction($formattedSession, $options); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/BeginTransaction', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getOptions(); - $this->assertProtobufEquals($options, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function beginTransactionExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $options = new TransactionOptions(); - try { - $gapicClient->beginTransaction($formattedSession, $options); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function commitTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new CommitResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $mutations = []; - $response = $gapicClient->commit($formattedSession, $mutations); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/Commit', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getMutations(); - $this->assertProtobufEquals($mutations, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function commitExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $mutations = []; - try { - $gapicClient->commit($formattedSession, $mutations); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function createSessionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name = 'name3373707'; - $creatorRole = 'creatorRole-1605962583'; - $multiplexed = false; - $expectedResponse = new Session(); - $expectedResponse->setName($name); - $expectedResponse->setCreatorRole($creatorRole); - $expectedResponse->setMultiplexed($multiplexed); - $transport->addResponse($expectedResponse); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $response = $gapicClient->createSession($formattedDatabase); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/CreateSession', $actualFuncCall); - $actualValue = $actualRequestObject->getDatabase(); - $this->assertProtobufEquals($formattedDatabase, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function createSessionExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - try { - $gapicClient->createSession($formattedDatabase); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteSessionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new GPBEmpty(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $gapicClient->deleteSession($formattedName); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/DeleteSession', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function deleteSessionExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - try { - $gapicClient->deleteSession($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function executeBatchDmlTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new ExecuteBatchDmlResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $transaction = new TransactionSelector(); - $statements = []; - $seqno = 109325920; - $response = $gapicClient->executeBatchDml($formattedSession, $transaction, $statements, $seqno); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/ExecuteBatchDml', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getTransaction(); - $this->assertProtobufEquals($transaction, $actualValue); - $actualValue = $actualRequestObject->getStatements(); - $this->assertProtobufEquals($statements, $actualValue); - $actualValue = $actualRequestObject->getSeqno(); - $this->assertProtobufEquals($seqno, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function executeBatchDmlExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $transaction = new TransactionSelector(); - $statements = []; - $seqno = 109325920; - try { - $gapicClient->executeBatchDml($formattedSession, $transaction, $statements, $seqno); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function executeSqlTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new ResultSet(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $sql = 'sql114126'; - $response = $gapicClient->executeSql($formattedSession, $sql); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/ExecuteSql', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getSql(); - $this->assertProtobufEquals($sql, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function executeSqlExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $sql = 'sql114126'; - try { - $gapicClient->executeSql($formattedSession, $sql); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function executeStreamingSqlTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $chunkedValue = true; - $resumeToken2 = '90'; - $last = true; - $expectedResponse = new PartialResultSet(); - $expectedResponse->setChunkedValue($chunkedValue); - $expectedResponse->setResumeToken($resumeToken2); - $expectedResponse->setLast($last); - $transport->addResponse($expectedResponse); - $chunkedValue2 = false; - $resumeToken3 = '91'; - $last2 = false; - $expectedResponse2 = new PartialResultSet(); - $expectedResponse2->setChunkedValue($chunkedValue2); - $expectedResponse2->setResumeToken($resumeToken3); - $expectedResponse2->setLast($last2); - $transport->addResponse($expectedResponse2); - $chunkedValue3 = true; - $resumeToken4 = '92'; - $last3 = true; - $expectedResponse3 = new PartialResultSet(); - $expectedResponse3->setChunkedValue($chunkedValue3); - $expectedResponse3->setResumeToken($resumeToken4); - $expectedResponse3->setLast($last3); - $transport->addResponse($expectedResponse3); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $sql = 'sql114126'; - $serverStream = $gapicClient->executeStreamingSql($formattedSession, $sql); - $this->assertInstanceOf(ServerStream::class, $serverStream); - $responses = iterator_to_array($serverStream->readAll()); - $expectedResponses = []; - $expectedResponses[] = $expectedResponse; - $expectedResponses[] = $expectedResponse2; - $expectedResponses[] = $expectedResponse3; - $this->assertEquals($expectedResponses, $responses); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/ExecuteStreamingSql', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getSql(); - $this->assertProtobufEquals($sql, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function executeStreamingSqlExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->setStreamingStatus($status); - $this->assertTrue($transport->isExhausted()); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $sql = 'sql114126'; - $serverStream = $gapicClient->executeStreamingSql($formattedSession, $sql); - $results = $serverStream->readAll(); - try { - iterator_to_array($results); - // If the close stream method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getSessionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $name2 = 'name2-1052831874'; - $creatorRole = 'creatorRole-1605962583'; - $multiplexed = false; - $expectedResponse = new Session(); - $expectedResponse->setName($name2); - $expectedResponse->setCreatorRole($creatorRole); - $expectedResponse->setMultiplexed($multiplexed); - $transport->addResponse($expectedResponse); - // Mock request - $formattedName = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $response = $gapicClient->getSession($formattedName); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/GetSession', $actualFuncCall); - $actualValue = $actualRequestObject->getName(); - $this->assertProtobufEquals($formattedName, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function getSessionExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedName = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - try { - $gapicClient->getSession($formattedName); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listSessionsTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $nextPageToken = ''; - $sessionsElement = new Session(); - $sessions = [ - $sessionsElement, - ]; - $expectedResponse = new ListSessionsResponse(); - $expectedResponse->setNextPageToken($nextPageToken); - $expectedResponse->setSessions($sessions); - $transport->addResponse($expectedResponse); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - $response = $gapicClient->listSessions($formattedDatabase); - $this->assertEquals($expectedResponse, $response->getPage()->getResponseObject()); - $resources = iterator_to_array($response->iterateAllElements()); - $this->assertSame(1, count($resources)); - $this->assertEquals($expectedResponse->getSessions()[0], $resources[0]); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/ListSessions', $actualFuncCall); - $actualValue = $actualRequestObject->getDatabase(); - $this->assertProtobufEquals($formattedDatabase, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function listSessionsExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedDatabase = $gapicClient->databaseName('[PROJECT]', '[INSTANCE]', '[DATABASE]'); - try { - $gapicClient->listSessions($formattedDatabase); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function partitionQueryTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new PartitionResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $sql = 'sql114126'; - $response = $gapicClient->partitionQuery($formattedSession, $sql); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/PartitionQuery', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getSql(); - $this->assertProtobufEquals($sql, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function partitionQueryExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $sql = 'sql114126'; - try { - $gapicClient->partitionQuery($formattedSession, $sql); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function partitionReadTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new PartitionResponse(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $table = 'table110115790'; - $keySet = new KeySet(); - $response = $gapicClient->partitionRead($formattedSession, $table, $keySet); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/PartitionRead', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getTable(); - $this->assertProtobufEquals($table, $actualValue); - $actualValue = $actualRequestObject->getKeySet(); - $this->assertProtobufEquals($keySet, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function partitionReadExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $table = 'table110115790'; - $keySet = new KeySet(); - try { - $gapicClient->partitionRead($formattedSession, $table, $keySet); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function readTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new ResultSet(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $table = 'table110115790'; - $columns = []; - $keySet = new KeySet(); - $response = $gapicClient->read($formattedSession, $table, $columns, $keySet); - $this->assertEquals($expectedResponse, $response); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/Read', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getTable(); - $this->assertProtobufEquals($table, $actualValue); - $actualValue = $actualRequestObject->getColumns(); - $this->assertProtobufEquals($columns, $actualValue); - $actualValue = $actualRequestObject->getKeySet(); - $this->assertProtobufEquals($keySet, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function readExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $table = 'table110115790'; - $columns = []; - $keySet = new KeySet(); - try { - $gapicClient->read($formattedSession, $table, $columns, $keySet); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function rollbackTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $expectedResponse = new GPBEmpty(); - $transport->addResponse($expectedResponse); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $transactionId = '28'; - $gapicClient->rollback($formattedSession, $transactionId); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/Rollback', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getTransactionId(); - $this->assertProtobufEquals($transactionId, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function rollbackExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->addResponse(null, $status); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $transactionId = '28'; - try { - $gapicClient->rollback($formattedSession, $transactionId); - // If the $gapicClient method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function streamingReadTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $this->assertTrue($transport->isExhausted()); - // Mock response - $chunkedValue = true; - $resumeToken2 = '90'; - $last = true; - $expectedResponse = new PartialResultSet(); - $expectedResponse->setChunkedValue($chunkedValue); - $expectedResponse->setResumeToken($resumeToken2); - $expectedResponse->setLast($last); - $transport->addResponse($expectedResponse); - $chunkedValue2 = false; - $resumeToken3 = '91'; - $last2 = false; - $expectedResponse2 = new PartialResultSet(); - $expectedResponse2->setChunkedValue($chunkedValue2); - $expectedResponse2->setResumeToken($resumeToken3); - $expectedResponse2->setLast($last2); - $transport->addResponse($expectedResponse2); - $chunkedValue3 = true; - $resumeToken4 = '92'; - $last3 = true; - $expectedResponse3 = new PartialResultSet(); - $expectedResponse3->setChunkedValue($chunkedValue3); - $expectedResponse3->setResumeToken($resumeToken4); - $expectedResponse3->setLast($last3); - $transport->addResponse($expectedResponse3); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $table = 'table110115790'; - $columns = []; - $keySet = new KeySet(); - $serverStream = $gapicClient->streamingRead($formattedSession, $table, $columns, $keySet); - $this->assertInstanceOf(ServerStream::class, $serverStream); - $responses = iterator_to_array($serverStream->readAll()); - $expectedResponses = []; - $expectedResponses[] = $expectedResponse; - $expectedResponses[] = $expectedResponse2; - $expectedResponses[] = $expectedResponse3; - $this->assertEquals($expectedResponses, $responses); - $actualRequests = $transport->popReceivedCalls(); - $this->assertSame(1, count($actualRequests)); - $actualFuncCall = $actualRequests[0]->getFuncCall(); - $actualRequestObject = $actualRequests[0]->getRequestObject(); - $this->assertSame('/google.spanner.v1.Spanner/StreamingRead', $actualFuncCall); - $actualValue = $actualRequestObject->getSession(); - $this->assertProtobufEquals($formattedSession, $actualValue); - $actualValue = $actualRequestObject->getTable(); - $this->assertProtobufEquals($table, $actualValue); - $actualValue = $actualRequestObject->getColumns(); - $this->assertProtobufEquals($columns, $actualValue); - $actualValue = $actualRequestObject->getKeySet(); - $this->assertProtobufEquals($keySet, $actualValue); - $this->assertTrue($transport->isExhausted()); - } - - /** @test */ - public function streamingReadExceptionTest() - { - $transport = $this->createTransport(); - $gapicClient = $this->createClient([ - 'transport' => $transport, - ]); - $status = new stdClass(); - $status->code = Code::DATA_LOSS; - $status->details = 'internal error'; - $expectedExceptionMessage = json_encode([ - 'message' => 'internal error', - 'code' => Code::DATA_LOSS, - 'status' => 'DATA_LOSS', - 'details' => [], - ], JSON_PRETTY_PRINT); - $transport->setStreamingStatus($status); - $this->assertTrue($transport->isExhausted()); - // Mock request - $formattedSession = $gapicClient->sessionName('[PROJECT]', '[INSTANCE]', '[DATABASE]', '[SESSION]'); - $table = 'table110115790'; - $columns = []; - $keySet = new KeySet(); - $serverStream = $gapicClient->streamingRead($formattedSession, $table, $columns, $keySet); - $results = $serverStream->readAll(); - try { - iterator_to_array($results); - // If the close stream method call did not throw, fail the test - $this->fail('Expected an ApiException, but no exception was thrown.'); - } catch (ApiException $ex) { - $this->assertEquals($status->code, $ex->getCode()); - $this->assertEquals($expectedExceptionMessage, $ex->getMessage()); - } - // Call popReceivedCalls to ensure the stub is exhausted - $transport->popReceivedCalls(); - $this->assertTrue($transport->isExhausted()); - } -} diff --git a/Spanner/tests/Unit/ValueMapperTest.php b/Spanner/tests/Unit/ValueMapperTest.php index 1ac13aa6407b..b2cf74c87213 100644 --- a/Spanner/tests/Unit/ValueMapperTest.php +++ b/Spanner/tests/Unit/ValueMapperTest.php @@ -17,7 +17,6 @@ namespace Google\Cloud\Spanner\Tests\Unit; -use DateInterval; use Google\Cloud\Core\Int64; use Google\Cloud\Core\Testing\GrpcTestTrait; use Google\Cloud\Spanner\ArrayType; @@ -32,14 +31,12 @@ use Google\Cloud\Spanner\StructType; use Google\Cloud\Spanner\StructValue; use Google\Cloud\Spanner\Timestamp; -use Google\Cloud\Spanner\ValueMapper; use Google\Cloud\Spanner\V1\TypeAnnotationCode; use Google\Cloud\Spanner\V1\TypeCode; -use InvalidArgumentException; +use Google\Cloud\Spanner\ValueMapper; use PHPUnit\Framework\TestCase; use Testing\Data\Book; use Testing\Data\User; -use RuntimeException; /** * @group spanner @@ -260,7 +257,7 @@ public function testFormatParamsForExecuteSqlArrayTypeNestedStruct() ]; $types = [ - 'foo' => new ArrayType((new StructType)->add('hello', Database::TYPE_STRING)) + 'foo' => new ArrayType((new StructType())->add('hello', Database::TYPE_STRING)) ]; $res = $this->mapper->formatParamsForExecuteSql($params, $types); @@ -367,7 +364,7 @@ public function testFormatParamsForExecuteSqlArrayMismatchedDefinition() $this->expectExceptionMessage('Array data does not match given array parameter type.'); $params = [ - 'foo' => [1,2,3] + 'foo' => [1, 2, 3] ]; $types = [ @@ -380,7 +377,7 @@ public function testFormatParamsForExecuteSqlArrayMismatchedDefinition() public function testFormatParamsForExecuteSqlArrayForCustomTypes() { $params = [ - 'foo' => [1,2,3] + 'foo' => [1, 2, 3] ]; $types = [ @@ -406,7 +403,7 @@ public function testFormatParamsForExecuteSqlArrayForCustomTypes() public function testFormatParamsForExecuteSqlArrayInvalidDefinition() { $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('Array parameter types must be an instance of Google\Cloud\Spanner\ArrayType.'); + $this->expectExceptionMessage('Array parameter types must be an instance of `Google\Cloud\Spanner\ArrayType`.'); $params = [ 'foo' => ['bar'] @@ -449,7 +446,7 @@ public function testFormatParamsForExecuteSqlStruct() ]; $types = [ - 'foo' => (new StructType) + 'foo' => (new StructType()) ->add('name', Database::TYPE_STRING) ->add('age', Database::TYPE_INT64) ->add('jobs', new ArrayType(Database::TYPE_STRING)) @@ -560,7 +557,7 @@ public function testFormatParamsForExecuteSqlStructInvalidDefinition() $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage( 'Struct parameter types must be declared explicitly, and must be an ' - . 'instance of Google\Cloud\Spanner\StructType.' + . 'instance of `Google\Cloud\Spanner\StructType`.' ); $params = [ 'foo' => [ @@ -591,7 +588,7 @@ public function testFormatParamsForExecuteSqlInvalidStructValue() ]; $types = [ - 'foo' => new StructType + 'foo' => new StructType() ]; $this->mapper->formatParamsForExecuteSql($params, $types); @@ -600,14 +597,14 @@ public function testFormatParamsForExecuteSqlInvalidStructValue() public function testFormatParamsForExecuteSqlStructDuplicateFieldNames() { $params = [ - 'foo' => (new StructValue) + 'foo' => (new StructValue()) ->add('hello', 'world') ->add('hello', 10) ->add('hello', 'goodbye') ]; $types = [ - 'foo' => (new StructType) + 'foo' => (new StructType()) ->add('hello', Database::TYPE_STRING) ->add('hello', Database::TYPE_INT64) ->add('hello', Database::TYPE_STRING) @@ -650,7 +647,7 @@ public function testFormatParamsForExecuteSqlStructDuplicateFieldNames() public function testFormatParamsForExecuteSqlStructUnnamedFields() { $params = [ - 'foo' => (new StructValue) + 'foo' => (new StructValue()) ->addUnnamed('hello') ->addUnnamed(10) ->add('key', 'val') @@ -658,7 +655,7 @@ public function testFormatParamsForExecuteSqlStructUnnamedFields() ]; $types = [ - 'foo' => (new StructType) + 'foo' => (new StructType()) ->add(null, Database::TYPE_STRING) ->addUnnamed(Database::TYPE_INT64) ->add('key', Database::TYPE_STRING) @@ -714,7 +711,7 @@ public function testFormatParamsForExecuteSqlInferredStructValueType() ]; $types = [ - 'foo' => (new StructType) + 'foo' => (new StructType()) ->add('hello', Database::TYPE_STRING) ->add('num', Database::TYPE_INT64) ]; @@ -750,14 +747,14 @@ public function testFormatParamsForExecuteSqlInferredStructValueType() public function testFormatParamsForExecuteSqlInferredStructValueTypeWithUnnamed() { $params = [ - 'foo' => (new StructValue) + 'foo' => (new StructValue()) ->add('hello', 'world') ->addUnnamed('foo') ->add('num', 10) ]; $types = [ - 'foo' => (new StructType) + 'foo' => (new StructType()) ->add('hello', Database::TYPE_STRING) ->add('num', Database::TYPE_INT64) ]; @@ -790,7 +787,7 @@ public function testFormatParamsForExecuteSqlStdClassValue() ]; $types = [ - 'foo' => (new StructType) + 'foo' => (new StructType()) ->add('hello', Database::TYPE_STRING) ]; @@ -1029,7 +1026,7 @@ public function testDecodeValuesString() public function testDecodeValuesTimestamp() { - $dt = new \DateTime; + $dt = new \DateTime(); $str = $dt->format(Timestamp::FORMAT); $res = $this->mapper->decodeValues( @@ -1044,7 +1041,7 @@ public function testDecodeValuesTimestamp() public function testDecodeValuesDate() { - $dt = new \DateTime; + $dt = new \DateTime(); $res = $this->mapper->decodeValues( $this->createField(Database::TYPE_DATE), $this->createRow($dt->format(Date::FORMAT)), @@ -1333,7 +1330,7 @@ public function testDecodeIntervalWithFractionField() /** @dataProvider invalidIntervals */ public function testIntervalWithInvalidFormatThrowsException(string $interval) { - $this->expectException(InvalidArgumentException::class); + $this->expectException(\InvalidArgumentException::class); $res = $this->mapper->decodeValues( $this->createField(Database::TYPE_INTERVAL), diff --git a/Spanner/tests/Unit/bootstrap.php b/Spanner/tests/Unit/bootstrap.php new file mode 100644 index 000000000000..f16f16a2c9c5 --- /dev/null +++ b/Spanner/tests/Unit/bootstrap.php @@ -0,0 +1,12 @@ +<?php + +use DG\BypassFinals; + +// Make sure that while testing we bypass the `final` keyword for the GAPIC client. +BypassFinals::setWhitelist([ + '*/src/Admin/Database/V1/Client/*', + '*/src/Admin/Instance/V1/Client/*', + '*/src/V1/Client/*', +]); + +BypassFinals::enable(); diff --git a/Spanner/tests/Unit/fixtures/instance.json b/Spanner/tests/Unit/fixtures/instance.json deleted file mode 100644 index fcf371769ce3..000000000000 --- a/Spanner/tests/Unit/fixtures/instance.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "projects\/test-project\/instances\/instance-name", - "config": "projects\/test-project\/instanceConfigs\/regional-europe-west1", - "displayName": "Instance Name", - "nodeCount": 1, - "state": 2 -} diff --git a/Spanner/tests/data/generated/GPBMetadata/Data/User.php b/Spanner/tests/data/generated/GPBMetadata/Data/User.php index e376ab39f821..b376d84c3b57 100644 --- a/Spanner/tests/data/generated/GPBMetadata/Data/User.php +++ b/Spanner/tests/data/generated/GPBMetadata/Data/User.php @@ -9,17 +9,18 @@ class User { public static $is_initialized = false; - public static function initOnce() { + public static function initOnce() + { $pool = \Google\Protobuf\Internal\DescriptorPool::getGeneratedPool(); if (static::$is_initialized == true) { - return; + return; } $pool->internalAddGeneratedFile( - "\x0A\xD6\x01\x0A\x0Fdata/user.proto\x12\x0Ctesting.data\"\x85\x01\x0A\x04User\x12\x0A\x0A\x02id\x18\x01 \x01(\x03\x12\x0C\x0A\x04name\x18\x02 \x01(\x09\x12\x0E\x0A\x06active\x18\x03 \x01(\x08\x12+\x0A\x07address\x18\x04 \x01(\x0B2\x1A.testing.data.User.Address\x1A&\x0A\x07Address\x12\x0C\x0A\x04city\x18\x01 \x01(\x09\x12\x0D\x0A\x05state\x18\x02 \x01(\x09\"%\x0A\x04Book\x12\x0D\x0A\x05title\x18\x01 \x01(\x09\x12\x0E\x0A\x06author\x18\x02 \x01(\x09b\x06proto3" - , true); + "\x0A\xD6\x01\x0A\x0Fdata/user.proto\x12\x0Ctesting.data\"\x85\x01\x0A\x04User\x12\x0A\x0A\x02id\x18\x01 \x01(\x03\x12\x0C\x0A\x04name\x18\x02 \x01(\x09\x12\x0E\x0A\x06active\x18\x03 \x01(\x08\x12+\x0A\x07address\x18\x04 \x01(\x0B2\x1A.testing.data.User.Address\x1A&\x0A\x07Address\x12\x0C\x0A\x04city\x18\x01 \x01(\x09\x12\x0D\x0A\x05state\x18\x02 \x01(\x09\"%\x0A\x04Book\x12\x0D\x0A\x05title\x18\x01 \x01(\x09\x12\x0E\x0A\x06author\x18\x02 \x01(\x09b\x06proto3", + true + ); static::$is_initialized = true; } } - diff --git a/Spanner/tests/data/generated/Testing/Data/Book.php b/Spanner/tests/data/generated/Testing/Data/Book.php index 5dee1612c25e..161587780022 100644 --- a/Spanner/tests/data/generated/Testing/Data/Book.php +++ b/Spanner/tests/data/generated/Testing/Data/Book.php @@ -5,8 +5,6 @@ namespace Testing\Data; -use Google\Protobuf\Internal\GPBType; -use Google\Protobuf\Internal\RepeatedField; use Google\Protobuf\Internal\GPBUtil; /** @@ -33,7 +31,8 @@ class Book extends \Google\Protobuf\Internal\Message * @type string $author * } */ - public function __construct($data = NULL) { + public function __construct($data = null) + { \GPBMetadata\Data\User::initOnce(); parent::__construct($data); } @@ -54,7 +53,7 @@ public function getTitle() */ public function setTitle($var) { - GPBUtil::checkString($var, True); + GPBUtil::checkString($var, true); $this->title = $var; return $this; @@ -76,11 +75,10 @@ public function getAuthor() */ public function setAuthor($var) { - GPBUtil::checkString($var, True); + GPBUtil::checkString($var, true); $this->author = $var; return $this; } } - diff --git a/Spanner/tests/data/generated/Testing/Data/User.php b/Spanner/tests/data/generated/Testing/Data/User.php index f093dff02c16..aaa0848c2e24 100644 --- a/Spanner/tests/data/generated/Testing/Data/User.php +++ b/Spanner/tests/data/generated/Testing/Data/User.php @@ -5,8 +5,6 @@ namespace Testing\Data; -use Google\Protobuf\Internal\GPBType; -use Google\Protobuf\Internal\RepeatedField; use Google\Protobuf\Internal\GPBUtil; /** @@ -43,7 +41,8 @@ class User extends \Google\Protobuf\Internal\Message * @type \Testing\Data\User\Address $address * } */ - public function __construct($data = NULL) { + public function __construct($data = null) + { \GPBMetadata\Data\User::initOnce(); parent::__construct($data); } @@ -86,7 +85,7 @@ public function getName() */ public function setName($var) { - GPBUtil::checkString($var, True); + GPBUtil::checkString($var, true); $this->name = $var; return $this; @@ -147,4 +146,3 @@ public function setAddress($var) } } - diff --git a/Spanner/tests/data/generated/Testing/Data/User/Address.php b/Spanner/tests/data/generated/Testing/Data/User/Address.php index d2391e7a6257..14a33c393862 100644 --- a/Spanner/tests/data/generated/Testing/Data/User/Address.php +++ b/Spanner/tests/data/generated/Testing/Data/User/Address.php @@ -5,8 +5,6 @@ namespace Testing\Data\User; -use Google\Protobuf\Internal\GPBType; -use Google\Protobuf\Internal\RepeatedField; use Google\Protobuf\Internal\GPBUtil; /** @@ -33,7 +31,8 @@ class Address extends \Google\Protobuf\Internal\Message * @type string $state * } */ - public function __construct($data = NULL) { + public function __construct($data = null) + { \GPBMetadata\Data\User::initOnce(); parent::__construct($data); } @@ -54,7 +53,7 @@ public function getCity() */ public function setCity($var) { - GPBUtil::checkString($var, True); + GPBUtil::checkString($var, true); $this->city = $var; return $this; @@ -76,11 +75,10 @@ public function getState() */ public function setState($var) { - GPBUtil::checkString($var, True); + GPBUtil::checkString($var, true); $this->state = $var; return $this; } } - diff --git a/composer.json b/composer.json index 0bb9248da4a3..a432e0171a62 100644 --- a/composer.json +++ b/composer.json @@ -63,6 +63,7 @@ "monolog/monolog": "^2.9||^3.0", "psr/http-message": "^1.0|^2.0", "ramsey/uuid": "^4.0", + "google/common-protos": "^4.4", "google/gax": "^1.38.0", "google/auth": "^1.42" }, @@ -78,7 +79,8 @@ "kreait/firebase-php": "^6.9", "psr/log": "^2.0||^3.0", "dg/bypass-finals": "^1.7", - "squizlabs/php_codesniffer": "3.*" + "squizlabs/php_codesniffer": "3.*", + "dms/phpunit-arraysubset-asserts": "^0.5.0" }, "replace": { "google/access-context-manager": "1.1.1",