From 539f4a003bcd52c7cb47a66c3051ff35977ce8cf Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 21 Nov 2016 15:14:10 -0500 Subject: [PATCH 001/107] Add Spanner VTK --- .../Admin/Database/V1/DatabaseAdminApi.php | 838 ++++++++++++ .../database_admin_client_config.json | 68 + .../Admin/Instance/V1/InstanceAdminApi.php | 1176 +++++++++++++++++ .../instance_admin_client_config.json | 78 ++ src/Spanner/V1/SpannerApi.php | 967 ++++++++++++++ .../V1/resources/spanner_client_config.json | 78 ++ 6 files changed, 3205 insertions(+) create mode 100644 src/Spanner/Admin/Database/V1/DatabaseAdminApi.php create mode 100644 src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json create mode 100644 src/Spanner/Admin/Instance/V1/InstanceAdminApi.php create mode 100644 src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json create mode 100644 src/Spanner/V1/SpannerApi.php create mode 100644 src/Spanner/V1/resources/spanner_client_config.json diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminApi.php b/src/Spanner/Admin/Database/V1/DatabaseAdminApi.php new file mode 100644 index 000000000000..ae6da1eb2f81 --- /dev/null +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminApi.php @@ -0,0 +1,838 @@ +listDatabases($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class DatabaseAdminApi +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + const _GAX_VERSION = '0.1.0'; + const _CODEGEN_NAME = 'GAPIC'; + const _CODEGEN_VERSION = '0.0.0'; + + private static $instanceNameTemplate; + private static $databaseNameTemplate; + + private $grpcCredentialsHelper; + private $databaseAdminStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + + /** + * Formats a string containing the fully-qualified path to represent + * a instance resource. + */ + public static function formatInstanceName($project, $instance) + { + return self::getInstanceNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a database resource. + */ + public static function formatDatabaseName($project, $instance, $database) + { + return self::getDatabaseNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseProjectFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseInstanceFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['instance']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a database resource. + */ + public static function parseProjectFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a database resource. + */ + public static function parseInstanceFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a database resource. + */ + public static function parseDatabaseFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['database']; + } + + private static function getInstanceNameTemplate() + { + if (self::$instanceNameTemplate == null) { + self::$instanceNameTemplate = new PathTemplate('projects/{project}/instances/{instance}'); + } + + return self::$instanceNameTemplate; + } + + private static function getDatabaseNameTemplate() + { + if (self::$databaseNameTemplate == null) { + self::$databaseNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}'); + } + + return self::$databaseNameTemplate; + } + + private static function getPageStreamingDescriptors() + { + $listDatabasesPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'databases', + ]); + + $pageStreamingDescriptors = [ + 'listDatabases' => $listDatabasesPageStreamingDescriptor, + ]; + + return $pageStreamingDescriptors; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'wrenchworks.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner Admin Database API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultScopes = [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ]; + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => $defaultScopes, + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => self::_GAX_VERSION, + 'credentialsLoader' => null, + ]; + $options = array_merge($defaultOptions, $options); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::_CODEGEN_NAME, + 'codeGenVersion' => self::_CODEGEN_VERSION, + 'gaxVersion' => self::_GAX_VERSION, + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'listDatabases' => $defaultDescriptors, + 'createDatabase' => $defaultDescriptors, + 'updateDatabase' => $defaultDescriptors, + 'dropDatabase' => $defaultDescriptors, + 'getDatabaseDDL' => $defaultDescriptors, + 'setIamPolicy' => $defaultDescriptors, + 'getIamPolicy' => $defaultDescriptors, + 'testIamPermissions' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/database_admin_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.admin.database.v1.DatabaseAdmin', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (!empty($options['sslCreds'])) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createDatabaseAdminStubFunction = function ($hostname, $opts) { + return new DatabaseAdminClient($hostname, $opts); + }; + $this->databaseAdminStub = $this->grpcCredentialsHelper->createStub( + $createDatabaseAdminStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Lists Cloud Spanner databases. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedName = DatabaseAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * foreach ($databaseAdminApi->listDatabases($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The project whose databases should be listed. Required. + * Values are of the form `projects//instances/`. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return Google\GAX\PagedListResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function listDatabases($name, $optionalArgs = []) + { + $request = new ListDatabasesRequest(); + $request->setName($name); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + + $mergedSettings = $this->defaultCallSettings['listDatabases']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'ListDatabases', + $mergedSettings, + $this->descriptors['listDatabases'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Creates a new Cloud Spanner database. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedName = DatabaseAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $createStatement = ""; + * $response = $databaseAdminApi->createDatabase($formattedName, $createStatement); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the instance that will serve the new database. + * Values are of the form `projects//instances/`. + * @param string $createStatement A `CREATE DATABASE` statement, which specifies the name of the + * new database. The database name must conform to the regular expression + * `[a-z][a-z0-9_\-]*[a-z0-9]` and be between 2 and 30 characters in length. + * @param array $optionalArgs { + * Optional. + * + * @type string[] $extraStatements + * An optional list of DDL statements to run inside the newly created + * database. Statements can create tables, indexes, etc. These + * statements execute atomically with the creation of the database: + * if there is an error in any statement, the database is not created. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\admin\database\v1\Database + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function createDatabase($name, $createStatement, $optionalArgs = []) + { + $request = new CreateDatabaseRequest(); + $request->setName($name); + $request->setCreateStatement($createStatement); + if (isset($optionalArgs['extraStatements'])) { + foreach ($optionalArgs['extraStatements'] as $elem) { + $request->addExtraStatements($elem); + } + } + + $mergedSettings = $this->defaultCallSettings['createDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'CreateDatabase', + $mergedSettings, + $this->descriptors['createDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Updates the schema of a Cloud Spanner database by + * creating/altering/dropping tables, columns, indexes, etc. The + * [UpdateDatabaseMetadata][google.spanner.admin.database.v1.UpdateDatabaseMetadata] message is used for operation + * metadata; The operation has no response. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $statements = []; + * $response = $databaseAdminApi->updateDatabase($formattedDatabase, $statements); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $database The database to update. + * @param string[] $statements DDL statements to be applied to the database. + * @param array $optionalArgs { + * Optional. + * + * @type string $operationId + * If empty, the new update request is assigned an + * automatically-generated operation ID. Otherwise, `operation_id` + * is used to construct the name of the resulting + * [Operation][google.longrunning.Operation]. + * + * Specifying an explicit operation ID simplifies determining + * whether the statements were executed in the event that the + * [UpdateDatabase][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase] call is replayed, + * or the return value is otherwise lost: the [database][google.spanner.admin.database.v1.UpdateDatabaseRequest.database] and + * `operation_id` fields can be combined to form the + * [name][google.longrunning.Operation.name] of the resulting + * [longrunning.Operation][google.longrunning.Operation]: `/operations/`. + * + * `operation_id` should be unique within the database, and must be + * a valid identifier: `[a-zA-Z][a-zA-Z0-9_]*`. Note that + * automatically-generated operation IDs always begin with an + * underscore. If the named operation already exists, + * [UpdateDatabase][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase] returns + * `ALREADY_EXISTS`. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\longrunning\Operation + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function updateDatabase($database, $statements, $optionalArgs = []) + { + $request = new UpdateDatabaseRequest(); + $request->setDatabase($database); + foreach ($statements as $elem) { + $request->addStatements($elem); + } + if (isset($optionalArgs['operationId'])) { + $request->setOperationId($optionalArgs['operationId']); + } + + $mergedSettings = $this->defaultCallSettings['updateDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'UpdateDatabase', + $mergedSettings, + $this->descriptors['updateDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Drops (aka deletes) a Cloud Spanner database. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $databaseAdminApi->dropDatabase($formattedDatabase); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $database The database to be dropped. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function dropDatabase($database, $optionalArgs = []) + { + $request = new DropDatabaseRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['dropDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'DropDatabase', + $mergedSettings, + $this->descriptors['dropDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns the schema of a Cloud Spanner database as a list of formatted + * DDL statements. This method does not show pending schema updates, those may + * be queried using the [Operations][google.longrunning.Operations] API. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminApi->getDatabaseDDL($formattedDatabase); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $database The database whose schema we wish to get. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\admin\database\v1\GetDatabaseDDLResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getDatabaseDDL($database, $optionalArgs = []) + { + $request = new GetDatabaseDDLRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['getDatabaseDDL']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'GetDatabaseDDL', + $mergedSettings, + $this->descriptors['getDatabaseDDL'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Sets the access control policy on a database resource. Replaces any + * existing policy. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $policy = new Policy(); + * $response = $databaseAdminApi->setIamPolicy($formattedResource, $policy); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being specified. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param Policy $policy REQUIRED: The complete policy to be applied to the `resource`. The size of + * the policy is limited to a few 10s of KB. An empty policy is a + * valid policy but certain Cloud Platform services (such as Projects) + * might reject them. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\Policy + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function setIamPolicy($resource, $policy, $optionalArgs = []) + { + $request = new SetIamPolicyRequest(); + $request->setResource($resource); + $request->setPolicy($policy); + + $mergedSettings = $this->defaultCallSettings['setIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'SetIamPolicy', + $mergedSettings, + $this->descriptors['setIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets the access control policy for a database resource. Returns an empty + * policy if a database exists but does not have a policy set. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminApi->getIamPolicy($formattedResource); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\Policy + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getIamPolicy($resource, $optionalArgs = []) + { + $request = new GetIamPolicyRequest(); + $request->setResource($resource); + + $mergedSettings = $this->defaultCallSettings['getIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'GetIamPolicy', + $mergedSettings, + $this->descriptors['getIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns permissions that the caller has on the specified database resource. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $permissions = []; + * $response = $databaseAdminApi->testIamPermissions($formattedResource, $permissions); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy detail is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see + * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\TestIamPermissionsResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function testIamPermissions($resource, $permissions, $optionalArgs = []) + { + $request = new TestIamPermissionsRequest(); + $request->setResource($resource); + foreach ($permissions as $elem) { + $request->addPermissions($elem); + } + + $mergedSettings = $this->defaultCallSettings['testIamPermissions']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'TestIamPermissions', + $mergedSettings, + $this->descriptors['testIamPermissions'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->databaseAdminStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json new file mode 100644 index 000000000000..b1a17f9f00c7 --- /dev/null +++ b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json @@ -0,0 +1,68 @@ +{ + "interfaces": { + "google.spanner.admin.database.v1.DatabaseAdmin": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "ListDatabases": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CreateDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "UpdateDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DropDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetDatabaseDDL": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "SetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "TestIamPermissions": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminApi.php b/src/Spanner/Admin/Instance/V1/InstanceAdminApi.php new file mode 100644 index 000000000000..f38aff05a78e --- /dev/null +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminApi.php @@ -0,0 +1,1176 @@ +listInstanceConfigs($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class InstanceAdminApi +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + const _GAX_VERSION = '0.1.0'; + const _CODEGEN_NAME = 'GAPIC'; + const _CODEGEN_VERSION = '0.0.0'; + + private static $projectNameTemplate; + private static $instanceConfigNameTemplate; + private static $instanceNameTemplate; + + private $grpcCredentialsHelper; + private $instanceAdminStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + + /** + * Formats a string containing the fully-qualified path to represent + * a project resource. + */ + public static function formatProjectName($project) + { + return self::getProjectNameTemplate()->render([ + 'project' => $project, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a instance_config resource. + */ + public static function formatInstanceConfigName($project, $instanceConfig) + { + return self::getInstanceConfigNameTemplate()->render([ + 'project' => $project, + 'instance_config' => $instanceConfig, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a instance resource. + */ + public static function formatInstanceName($project, $instance) + { + return self::getInstanceNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a project resource. + */ + public static function parseProjectFromProjectName($projectName) + { + return self::getProjectNameTemplate()->match($projectName)['project']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instanceConfig resource. + */ + public static function parseProjectFromInstanceConfigName($instanceConfigName) + { + return self::getInstanceConfigNameTemplate()->match($instanceConfigName)['project']; + } + + /** + * Parses the instance_config from the given fully-qualified path which + * represents a instanceConfig resource. + */ + public static function parseInstanceConfigFromInstanceConfigName($instanceConfigName) + { + return self::getInstanceConfigNameTemplate()->match($instanceConfigName)['instance_config']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseProjectFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseInstanceFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['instance']; + } + + private static function getProjectNameTemplate() + { + if (self::$projectNameTemplate == null) { + self::$projectNameTemplate = new PathTemplate('projects/{project}'); + } + + return self::$projectNameTemplate; + } + + private static function getInstanceConfigNameTemplate() + { + if (self::$instanceConfigNameTemplate == null) { + self::$instanceConfigNameTemplate = new PathTemplate('projects/{project}/instanceConfigs/{instance_config}'); + } + + return self::$instanceConfigNameTemplate; + } + + private static function getInstanceNameTemplate() + { + if (self::$instanceNameTemplate == null) { + self::$instanceNameTemplate = new PathTemplate('projects/{project}/instances/{instance}'); + } + + return self::$instanceNameTemplate; + } + + private static function getPageStreamingDescriptors() + { + $listInstanceConfigsPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'instance_configs', + ]); + $listInstancesPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'instances', + ]); + + $pageStreamingDescriptors = [ + 'listInstanceConfigs' => $listInstanceConfigsPageStreamingDescriptor, + 'listInstances' => $listInstancesPageStreamingDescriptor, + ]; + + return $pageStreamingDescriptors; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'wrenchworks.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner Admin Instance API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultScopes = [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ]; + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => $defaultScopes, + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => self::_GAX_VERSION, + 'credentialsLoader' => null, + ]; + $options = array_merge($defaultOptions, $options); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::_CODEGEN_NAME, + 'codeGenVersion' => self::_CODEGEN_VERSION, + 'gaxVersion' => self::_GAX_VERSION, + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'listInstanceConfigs' => $defaultDescriptors, + 'getInstanceConfig' => $defaultDescriptors, + 'listInstances' => $defaultDescriptors, + 'getInstance' => $defaultDescriptors, + 'createInstance' => $defaultDescriptors, + 'updateInstance' => $defaultDescriptors, + 'deleteInstance' => $defaultDescriptors, + 'setIamPolicy' => $defaultDescriptors, + 'getIamPolicy' => $defaultDescriptors, + 'testIamPermissions' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/instance_admin_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.admin.instance.v1.InstanceAdmin', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (!empty($options['sslCreds'])) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createInstanceAdminStubFunction = function ($hostname, $opts) { + return new InstanceAdminClient($hostname, $opts); + }; + $this->instanceAdminStub = $this->grpcCredentialsHelper->createStub( + $createInstanceAdminStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Lists the supported instance configurations for a given project. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatProjectName("[PROJECT]"); + * foreach ($instanceAdminApi->listInstanceConfigs($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the project for which a list of supported instance + * configurations is requested. Values are of the form + * `projects/`. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return Google\GAX\PagedListResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function listInstanceConfigs($name, $optionalArgs = []) + { + $request = new ListInstanceConfigsRequest(); + $request->setName($name); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + + $mergedSettings = $this->defaultCallSettings['listInstanceConfigs']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'ListInstanceConfigs', + $mergedSettings, + $this->descriptors['listInstanceConfigs'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets information about a particular instance configuration. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceConfigName("[PROJECT]", "[INSTANCE_CONFIG]"); + * $response = $instanceAdminApi->getInstanceConfig($formattedName); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the requested instance configuration. Values are of the form + * `projects//instanceConfigs/`. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\admin\instance\v1\InstanceConfig + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getInstanceConfig($name, $optionalArgs = []) + { + $request = new GetInstanceConfigRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getInstanceConfig']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetInstanceConfig', + $mergedSettings, + $this->descriptors['getInstanceConfig'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Lists all instances in the given project. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatProjectName("[PROJECT]"); + * foreach ($instanceAdminApi->listInstances($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name 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 is howl. + * * name:HOWL --> Equivalent to above. + * * NAME:howl --> Equivalent to above. + * * labels.env:* --> The instance has the label env. + * * labels.env:dev --> The instance's label env has the value dev. + * * name:howl labels.env:dev --> The instance's name is howl and it has + * the label env with value dev. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return Google\GAX\PagedListResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function listInstances($name, $optionalArgs = []) + { + $request = new ListInstancesRequest(); + $request->setName($name); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + if (isset($optionalArgs['filter'])) { + $request->setFilter($optionalArgs['filter']); + } + + $mergedSettings = $this->defaultCallSettings['listInstances']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'ListInstances', + $mergedSettings, + $this->descriptors['listInstances'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets information about a particular instance. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $response = $instanceAdminApi->getInstance($formattedName); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the requested instance. Values are of the form + * `projects//instances/`. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\admin\instance\v1\Instance + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getInstance($name, $optionalArgs = []) + { + $request = new GetInstanceRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetInstance', + $mergedSettings, + $this->descriptors['getInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Creates an instance and begins preparing it to begin serving. The + * returned [long-running operation][google.longrunning.Operation] + * can be used to track the progress of preparing the new + * instance. The instance name is assigned by the caller. If the + * named instance already exists, `CreateInstance` returns + * `ALREADY_EXISTS`. + * + * Immediately upon completion of this request: + * + * * The instance is readable via the API, with all requested attributes + * but no allocated resources. Its state is `CREATING`. + * + * Until completion of the returned operation: + * + * * Cancelling the operation renders the instance immediately unreadable + * via the API. + * * The instance can be deleted. + * * All other attempts to modify the instance are rejected. + * + * Upon completion of the returned operation: + * + * * Billing for all successfully-allocated resources begins (some types + * may have lower than the requested levels). + * * Databases can be created in the instance. + * * The instance's allocated resource levels are readable via the API. + * * The instance's state becomes `READY`. + * + * The returned operation's + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata] + * The returned operation's + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if + * successful. + * + * Authorization requires `spanner.instances.create` permission on + * resource [name][google.spanner.admin.instance.v1.Instance.name]. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $config = ""; + * $displayName = ""; + * $nodeCount = 0; + * $response = $instanceAdminApi->createInstance($formattedName, $config, $displayName, $nodeCount); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name A unique identifier for the instance, which cannot be changed after + * the instance is created. Values are of the form + * `projects//instances/[a-z][-a-z0-9]*[a-z0-9]`. The final + * segment of the name must be between 6 and 30 characters in length. + * @param string $config The name of the instance's configuration. Values are of the form + * `projects//instanceConfigs/`. See + * also [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig] and + * [ListInstanceConfigs][google.spanner.admin.instance.v1.InstanceAdmin.ListInstanceConfigs]. + * @param string $displayName The descriptive name for this instance as it appears in UIs. + * Must be unique per project and between 4 and 30 characters in length. + * @param int $nodeCount The number of nodes allocated to this instance. + * @param array $optionalArgs { + * Optional. + * + * @type State $state + * The current instance state. For + * [CreateInstance][google.spanner.admin.instance.v1.InstanceAdmin.CreateInstance], the state must be + * either omitted or set to `CREATING`. For + * [UpdateInstance][google.spanner.admin.instance.v1.InstanceAdmin.UpdateInstance], the state must be + * either omitted or set to `READY`. + * @type array $labels + * Cloud Labels are a flexible and lightweight mechanism for organizing cloud + * resources into groups that reflect a customer's organizational needs and + * deployment strategies. Cloud Labels can be used to filter collections of + * resources. They can be used to control how resource metrics are aggregated. + * And they can be used as arguments to policy management rules (e.g. route, + * firewall, load balancing, etc.). + * + * * Label keys must be between 1 and 63 characters long and must conform to + * the following regular expression: `[a-z]([-a-z0-9]*[a-z0-9])?`. + * * Label values must be between 0 and 63 characters long and must conform + * to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. + * * No more than 64 labels can be associated with a given resource. + * + * See https://goo.gl/xmQnxf for more information on and examples of labels. + * + * If you plan to use labels in your own code, please note that additional + * characters may be allowed in the future. And so you are advised to use an + * internal label representation, such as JSON, which doesn't rely upon + * specific characters being disallowed. For example, representing labels + * as the string: name + "_" + value would prove problematic if we were to + * allow "_" in a future release. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\longrunning\Operation + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function createInstance($name, $config, $displayName, $nodeCount, $optionalArgs = []) + { + $request = new Instance(); + $request->setName($name); + $request->setConfig($config); + $request->setDisplayName($displayName); + $request->setNodeCount($nodeCount); + if (isset($optionalArgs['state'])) { + $request->setState($optionalArgs['state']); + } + if (isset($optionalArgs['labels'])) { + foreach ($optionalArgs['labels'] as $key => $value) { + $request->addLabels((new LabelsEntry())->setKey($key)->setValue($value)); + } + } + + $mergedSettings = $this->defaultCallSettings['createInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'CreateInstance', + $mergedSettings, + $this->descriptors['createInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Updates an instance, and begins allocating or releasing resources + * as requested. The returned [long-running + * operation][google.longrunning.Operation] can be used to track the + * progress of updating the instance. If the named instance does not + * exist, returns `NOT_FOUND`. + * + * Immediately upon completion of this request: + * + * * For resource types for which a decrease in the instance's allocation + * has been requested, billing is based on the newly-requested level. + * + * Until completion of the returned operation: + * + * * Cancelling the operation sets its metadata's + * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins + * restoring resources to their pre-request values. The operation + * is guaranteed to succeed at undoing all resource changes, + * after which point it terminates with a `CANCELLED` status. + * * All other attempts to modify the instance are rejected. + * * Reading the instance via the API continues to give the pre-request + * resource levels. + * + * Upon completion of the returned operation: + * + * * Billing begins for all successfully-allocated resources (some types + * may have lower than the requested levels). + * * All newly-reserved resources are available for serving the instance's + * tables. + * * The instance's new resource levels are readable via the API. + * + * The returned operation's + * [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata] + * The returned operation's + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if + * successful. + * + * Authorization requires `spanner.instances.update` permission on + * resource [name][google.spanner.admin.instance.v1.Instance.name]. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $config = ""; + * $displayName = ""; + * $nodeCount = 0; + * $response = $instanceAdminApi->updateInstance($formattedName, $config, $displayName, $nodeCount); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name A unique identifier for the instance, which cannot be changed after + * the instance is created. Values are of the form + * `projects//instances/[a-z][-a-z0-9]*[a-z0-9]`. The final + * segment of the name must be between 6 and 30 characters in length. + * @param string $config The name of the instance's configuration. Values are of the form + * `projects//instanceConfigs/`. See + * also [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig] and + * [ListInstanceConfigs][google.spanner.admin.instance.v1.InstanceAdmin.ListInstanceConfigs]. + * @param string $displayName The descriptive name for this instance as it appears in UIs. + * Must be unique per project and between 4 and 30 characters in length. + * @param int $nodeCount The number of nodes allocated to this instance. + * @param array $optionalArgs { + * Optional. + * + * @type State $state + * The current instance state. For + * [CreateInstance][google.spanner.admin.instance.v1.InstanceAdmin.CreateInstance], the state must be + * either omitted or set to `CREATING`. For + * [UpdateInstance][google.spanner.admin.instance.v1.InstanceAdmin.UpdateInstance], the state must be + * either omitted or set to `READY`. + * @type array $labels + * Cloud Labels are a flexible and lightweight mechanism for organizing cloud + * resources into groups that reflect a customer's organizational needs and + * deployment strategies. Cloud Labels can be used to filter collections of + * resources. They can be used to control how resource metrics are aggregated. + * And they can be used as arguments to policy management rules (e.g. route, + * firewall, load balancing, etc.). + * + * * Label keys must be between 1 and 63 characters long and must conform to + * the following regular expression: `[a-z]([-a-z0-9]*[a-z0-9])?`. + * * Label values must be between 0 and 63 characters long and must conform + * to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. + * * No more than 64 labels can be associated with a given resource. + * + * See https://goo.gl/xmQnxf for more information on and examples of labels. + * + * If you plan to use labels in your own code, please note that additional + * characters may be allowed in the future. And so you are advised to use an + * internal label representation, such as JSON, which doesn't rely upon + * specific characters being disallowed. For example, representing labels + * as the string: name + "_" + value would prove problematic if we were to + * allow "_" in a future release. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\longrunning\Operation + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function updateInstance($name, $config, $displayName, $nodeCount, $optionalArgs = []) + { + $request = new Instance(); + $request->setName($name); + $request->setConfig($config); + $request->setDisplayName($displayName); + $request->setNodeCount($nodeCount); + if (isset($optionalArgs['state'])) { + $request->setState($optionalArgs['state']); + } + if (isset($optionalArgs['labels'])) { + foreach ($optionalArgs['labels'] as $key => $value) { + $request->addLabels((new LabelsEntry())->setKey($key)->setValue($value)); + } + } + + $mergedSettings = $this->defaultCallSettings['updateInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'UpdateInstance', + $mergedSettings, + $this->descriptors['updateInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Deletes an instance. + * + * Immediately upon completion of the request: + * + * * Billing ceases for all of the instance's reserved resources. + * + * Soon afterward: + * + * * The instance and *all of its databases* immediately and + * irrevocably disappear from the API. All data in the databases + * is permanently deleted. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $instanceAdminApi->deleteInstance($formattedName); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the instance to be deleted. Values are of the form + * `projects//instances/` + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function deleteInstance($name, $optionalArgs = []) + { + $request = new DeleteInstanceRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['deleteInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'DeleteInstance', + $mergedSettings, + $this->descriptors['deleteInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Sets the access control policy on an instance resource. Replaces any + * existing policy. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $policy = new Policy(); + * $response = $instanceAdminApi->setIamPolicy($formattedResource, $policy); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being specified. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param Policy $policy REQUIRED: The complete policy to be applied to the `resource`. The size of + * the policy is limited to a few 10s of KB. An empty policy is a + * valid policy but certain Cloud Platform services (such as Projects) + * might reject them. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\Policy + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function setIamPolicy($resource, $policy, $optionalArgs = []) + { + $request = new SetIamPolicyRequest(); + $request->setResource($resource); + $request->setPolicy($policy); + + $mergedSettings = $this->defaultCallSettings['setIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'SetIamPolicy', + $mergedSettings, + $this->descriptors['setIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets the access control policy for an instance resource. Returns an empty + * policy if an instance exists but does not have a policy set. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $response = $instanceAdminApi->getIamPolicy($formattedResource); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\Policy + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getIamPolicy($resource, $optionalArgs = []) + { + $request = new GetIamPolicyRequest(); + $request->setResource($resource); + + $mergedSettings = $this->defaultCallSettings['getIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetIamPolicy', + $mergedSettings, + $this->descriptors['getIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns permissions that the caller has on the specified instance resource. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $permissions = []; + * $response = $instanceAdminApi->testIamPermissions($formattedResource, $permissions); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy detail is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see + * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\TestIamPermissionsResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function testIamPermissions($resource, $permissions, $optionalArgs = []) + { + $request = new TestIamPermissionsRequest(); + $request->setResource($resource); + foreach ($permissions as $elem) { + $request->addPermissions($elem); + } + + $mergedSettings = $this->defaultCallSettings['testIamPermissions']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'TestIamPermissions', + $mergedSettings, + $this->descriptors['testIamPermissions'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->instanceAdminStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json new file mode 100644 index 000000000000..2aa03b47e61d --- /dev/null +++ b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json @@ -0,0 +1,78 @@ +{ + "interfaces": { + "google.spanner.admin.instance.v1.InstanceAdmin": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "ListInstanceConfigs": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetInstanceConfig": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "ListInstances": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetInstance": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CreateInstance": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "UpdateInstance": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "DeleteInstance": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "SetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "TestIamPermissions": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/Spanner/V1/SpannerApi.php b/src/Spanner/V1/SpannerApi.php new file mode 100644 index 000000000000..4607be193f4d --- /dev/null +++ b/src/Spanner/V1/SpannerApi.php @@ -0,0 +1,967 @@ +createSession($formattedDatabase); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class SpannerApi +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + const _GAX_VERSION = '0.1.0'; + const _CODEGEN_NAME = 'GAPIC'; + const _CODEGEN_VERSION = '0.0.0'; + + private static $databaseNameTemplate; + private static $sessionNameTemplate; + + private $grpcCredentialsHelper; + private $spannerStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + + /** + * Formats a string containing the fully-qualified path to represent + * a database resource. + */ + public static function formatDatabaseName($project, $instance, $database) + { + return self::getDatabaseNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a session resource. + */ + public static function formatSessionName($project, $instance, $database, $session) + { + return self::getSessionNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + 'session' => $session, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a database resource. + */ + public static function parseProjectFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a database resource. + */ + public static function parseInstanceFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a database resource. + */ + public static function parseDatabaseFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['database']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a session resource. + */ + public static function parseProjectFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a session resource. + */ + public static function parseInstanceFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a session resource. + */ + public static function parseDatabaseFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['database']; + } + + /** + * Parses the session from the given fully-qualified path which + * represents a session resource. + */ + public static function parseSessionFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['session']; + } + + private static function getDatabaseNameTemplate() + { + if (self::$databaseNameTemplate == null) { + self::$databaseNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}'); + } + + return self::$databaseNameTemplate; + } + + private static function getSessionNameTemplate() + { + if (self::$sessionNameTemplate == null) { + self::$sessionNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}/sessions/{session}'); + } + + return self::$sessionNameTemplate; + } + + private static function getPageStreamingDescriptors() + { + $pageStreamingDescriptors = [ + ]; + + return $pageStreamingDescriptors; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'wrenchworks.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultScopes = [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.data', + ]; + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => $defaultScopes, + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => self::_GAX_VERSION, + 'credentialsLoader' => null, + ]; + $options = array_merge($defaultOptions, $options); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::_CODEGEN_NAME, + 'codeGenVersion' => self::_CODEGEN_VERSION, + 'gaxVersion' => self::_GAX_VERSION, + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'createSession' => $defaultDescriptors, + 'getSession' => $defaultDescriptors, + 'deleteSession' => $defaultDescriptors, + 'executeSql' => $defaultDescriptors, + 'read' => $defaultDescriptors, + 'beginTransaction' => $defaultDescriptors, + 'commit' => $defaultDescriptors, + 'rollback' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/spanner_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.v1.Spanner', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (!empty($options['sslCreds'])) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createSpannerStubFunction = function ($hostname, $opts) { + return new SpannerClient($hostname, $opts); + }; + $this->spannerStub = $this->grpcCredentialsHelper->createStub( + $createSpannerStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Creates a new session. A session can be used to perform + * transactions that read and/or modify data in a Cloud Spanner database. + * Sessions are meant to be reused for many consecutive + * transactions. + * + * Sessions can only execute one transaction at a time. To execute + * multiple concurrent read-write/write-only transactions, create + * multiple sessions. Note that standalone reads and queries use a + * transaction internally, and count toward the one transaction + * limit. + * + * Cloud Spanner limits the number of sessions that can exist at any given + * time; thus, it is a good idea to delete idle and/or unneeded sessions. + * Aside from explicit deletes, Cloud Spanner can delete sessions for + * which no operations are sent for more than an hour, or due to + * internal errors. If a session is deleted, requests to it + * return `NOT_FOUND`. + * + * Idle sessions can be kept alive by sending a trivial SQL query + * periodically, e.g., `"SELECT 1"`. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedDatabase = SpannerApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $spannerApi->createSession($formattedDatabase); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $database The database in which the new session is created. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\Session + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function createSession($database, $optionalArgs = []) + { + $request = new CreateSessionRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['createSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'CreateSession', + $mergedSettings, + $this->descriptors['createSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets a session. Returns `NOT_FOUND` if the session does not exist. + * This is mainly useful for determining whether a session is still + * alive. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedName = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $response = $spannerApi->getSession($formattedName); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the session to retrieve. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\Session + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getSession($name, $optionalArgs = []) + { + $request = new GetSessionRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'GetSession', + $mergedSettings, + $this->descriptors['getSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Ends a session, releasing server resources associated with it. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedName = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerApi->deleteSession($formattedName); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the session to delete. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function deleteSession($name, $optionalArgs = []) + { + $request = new DeleteSessionRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['deleteSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'DeleteSession', + $mergedSettings, + $this->descriptors['deleteSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Executes an SQL query, returning all rows in a single reply. This + * method cannot be used to return a result set larger than 10 MiB; + * if the query yields more data than that, the query fails with + * a `FAILED_PRECONDITION` error. + * + * Queries inside read-write transactions might return `ABORTED`. If + * this occurs, the application should restart the transaction from + * the beginning. See [Transaction][google.spanner.v1.Transaction] for more details. + * + * Larger result sets can be fetched in streaming fashion by calling + * [ExecuteStreamingSql][google.spanner.v1.Spanner.ExecuteStreamingSql] instead. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $sql = ""; + * $response = $spannerApi->executeSql($formattedSession, $sql); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the SQL query should be performed. + * @param string $sql The SQL query string. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type Struct $params + * The SQL query string can contain parameter placeholders. A parameter + * placeholder consists of `'@'` followed by the parameter + * name. Parameter names consist of any combination of letters, + * numbers, and underscores. + * + * Parameters can appear anywhere that a literal value is expected. The same + * parameter name can be used more than once, for example: + * `"WHERE id > @msg_id AND id < @msg_id + 100"` + * + * It is an error to execute an SQL query with unbound parameters. + * + * Parameter values are specified using `params`, which is a JSON + * object whose keys are parameter names, and whose values are the + * corresponding parameter values. + * @type array $paramTypes + * It is not always possible for Cloud Spanner to infer the right SQL type + * from a JSON value. For example, values of type `BYTES` and values + * of type `STRING` both appear in [params][google.spanner.v1.ExecuteSqlRequest.params] as JSON strings. + * + * In these cases, `param_types` can be used to specify the exact + * SQL type for some or all of the SQL query parameters. See the + * definition of [Type][google.spanner.v1.Type] for more information + * about SQL types. + * @type string $resumeToken + * If this request is resuming a previously interrupted SQL query + * execution, `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new SQL query execution to resume where the last one left + * off. The rest of the request parameters must exactly match the + * request that yielded this token. + * @type QueryMode $queryMode + * Used to control the amount of debugging information returned in + * [ResultSetStats][google.spanner.v1.ResultSetStats]. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\ResultSet + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function executeSql($session, $sql, $optionalArgs = []) + { + $request = new ExecuteSqlRequest(); + $request->setSession($session); + $request->setSql($sql); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['params'])) { + $request->setParams($optionalArgs['params']); + } + if (isset($optionalArgs['paramTypes'])) { + foreach ($optionalArgs['paramTypes'] as $key => $value) { + $request->addParamTypes((new ParamTypesEntry())->setKey($key)->setValue($value)); + } + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + if (isset($optionalArgs['queryMode'])) { + $request->setQueryMode($optionalArgs['queryMode']); + } + + $mergedSettings = $this->defaultCallSettings['executeSql']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'ExecuteSql', + $mergedSettings, + $this->descriptors['executeSql'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Reads rows from the database using key lookups and scans, as a + * simple key/value style alternative to + * [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql]. This method cannot be used to + * return a result set larger than 10 MiB; if the read matches more + * data than that, the read fails with a `FAILED_PRECONDITION` + * error. + * + * Reads inside read-write transactions might return `ABORTED`. If + * this occurs, the application should restart the transaction from + * the beginning. See [Transaction][google.spanner.v1.Transaction] for more details. + * + * Larger result sets can be yielded in streaming fashion by calling + * [StreamingRead][google.spanner.v1.Spanner.StreamingRead] instead. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $table = ""; + * $columns = []; + * $keySet = new KeySet(); + * $response = $spannerApi->read($formattedSession, $table, $columns, $keySet); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the read should be performed. + * @param string $table The name of the table in the database to be read. Must be non-empty. + * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching + * this request. + * @param KeySet $keySet `key_set` identifies the rows to be yielded. `key_set` names the + * primary keys of the rows in [table][google.spanner.v1.ReadRequest.table] to be yielded, unless [index][google.spanner.v1.ReadRequest.index] + * is present. If [index][google.spanner.v1.ReadRequest.index] is present, then [key_set][google.spanner.v1.ReadRequest.key_set] instead names + * index keys in [index][google.spanner.v1.ReadRequest.index]. + * + * Rows are yielded in table primary key order (if [index][google.spanner.v1.ReadRequest.index] is empty) + * or index key order (if [index][google.spanner.v1.ReadRequest.index] is non-empty). + * + * It is not an error for the `key_set` to name rows that do not + * exist in the database. Read yields nothing for nonexistent rows. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type string $index + * If non-empty, the name of an index on [table][google.spanner.v1.ReadRequest.table]. This index is + * used instead of the table primary key when interpreting [key_set][google.spanner.v1.ReadRequest.key_set] + * and sorting result rows. See [key_set][google.spanner.v1.ReadRequest.key_set] for further information. + * @type int $offset + * The first `offset` rows matching [key_set][google.spanner.v1.ReadRequest.key_set] are skipped. Note + * that the implementation must read the rows in order to skip + * them. Where possible, it is much more efficient to adjust [key_set][google.spanner.v1.ReadRequest.key_set] + * to exclude unwanted rows. + * @type int $limit + * If greater than zero, after skipping the first [offset][google.spanner.v1.ReadRequest.offset] rows, + * only the next `limit` rows are yielded. If `limit` is zero, + * the default is no limit. + * @type string $resumeToken + * If this request is resuming a previously interrupted read, + * `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new read to resume where the last read left off. The + * rest of the request parameters must exactly match the request + * that yielded this token. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\ResultSet + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function read($session, $table, $columns, $keySet, $optionalArgs = []) + { + $request = new ReadRequest(); + $request->setSession($session); + $request->setTable($table); + foreach ($columns as $elem) { + $request->addColumns($elem); + } + $request->setKeySet($keySet); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['index'])) { + $request->setIndex($optionalArgs['index']); + } + if (isset($optionalArgs['offset'])) { + $request->setOffset($optionalArgs['offset']); + } + if (isset($optionalArgs['limit'])) { + $request->setLimit($optionalArgs['limit']); + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + + $mergedSettings = $this->defaultCallSettings['read']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Read', + $mergedSettings, + $this->descriptors['read'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Begins a new transaction. This step can often be skipped: + * [Read][google.spanner.v1.Spanner.Read], [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] and + * [Commit][google.spanner.v1.Spanner.Commit] can begin a new transaction as a + * side-effect. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $options = new TransactionOptions(); + * $response = $spannerApi->beginTransaction($formattedSession, $options); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the transaction runs. + * @param TransactionOptions $options Options for the new transaction. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\Transaction + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function beginTransaction($session, $options, $optionalArgs = []) + { + $request = new BeginTransactionRequest(); + $request->setSession($session); + $request->setOptions($options); + + $mergedSettings = $this->defaultCallSettings['beginTransaction']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'BeginTransaction', + $mergedSettings, + $this->descriptors['beginTransaction'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Commits a transaction. The request includes the mutations to be + * applied to rows in the database. + * + * `Commit` might return an `ABORTED` error. This can occur at any time; + * commonly, the cause is conflicts with concurrent + * transactions. However, it can also happen for a variety of other + * reasons. If `Commit` returns `ABORTED`, the caller should re-attempt + * the transaction from the beginning, re-using the same session. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $mutations = []; + * $response = $spannerApi->commit($formattedSession, $mutations); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the transaction to be committed is running. + * @param Mutation[] $mutations The mutations to be executed when this transaction commits. All + * mutations are applied atomically, in the order they appear in + * this list. + * @param array $optionalArgs { + * Optional. + * + * @type string $transactionId + * Commit a previously-started transaction. + * @type TransactionOptions $singleUseTransaction + * Execute mutations in a temporary transaction. Note that unlike + * commit of a previously-started transaction, commit with a + * temporary transaction is non-idempotent. That is, if the + * `CommitRequest` is sent to Cloud Spanner more than once (for + * instance, due to retries in the application, or in the + * transport library), it is possible that the mutations are + * executed more than once. If this is undesirable, use + * [BeginTransaction][google.spanner.v1.Spanner.BeginTransaction] and + * [Commit][google.spanner.v1.Spanner.Commit] instead. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\CommitResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function commit($session, $mutations, $optionalArgs = []) + { + $request = new CommitRequest(); + $request->setSession($session); + foreach ($mutations as $elem) { + $request->addMutations($elem); + } + if (isset($optionalArgs['transactionId'])) { + $request->setTransactionId($optionalArgs['transactionId']); + } + if (isset($optionalArgs['singleUseTransaction'])) { + $request->setSingleUseTransaction($optionalArgs['singleUseTransaction']); + } + + $mergedSettings = $this->defaultCallSettings['commit']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Commit', + $mergedSettings, + $this->descriptors['commit'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Rolls back a transaction, releasing any locks it holds. It is a good + * idea to call this for any transaction that includes one or more + * [Read][google.spanner.v1.Spanner.Read] or [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] requests and + * ultimately decides not to commit. + * + * `Rollback` returns `OK` if it successfully aborts the transaction, the + * transaction was already aborted, or the transaction is not + * found. `Rollback` never returns `ABORTED`. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $transactionId = ""; + * $spannerApi->rollback($formattedSession, $transactionId); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the transaction to roll back is running. + * @param string $transactionId The transaction to roll back. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function rollback($session, $transactionId, $optionalArgs = []) + { + $request = new RollbackRequest(); + $request->setSession($session); + $request->setTransactionId($transactionId); + + $mergedSettings = $this->defaultCallSettings['rollback']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Rollback', + $mergedSettings, + $this->descriptors['rollback'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->spannerStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/V1/resources/spanner_client_config.json b/src/Spanner/V1/resources/spanner_client_config.json new file mode 100644 index 000000000000..6299ccfa6961 --- /dev/null +++ b/src/Spanner/V1/resources/spanner_client_config.json @@ -0,0 +1,78 @@ +{ + "interfaces": { + "google.spanner.v1.Spanner": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "CreateSession": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetSession": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DeleteSession": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "ExecuteSql": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "ExecuteStreamingSql": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Read": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "StreamingRead": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "BeginTransaction": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Commit": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Rollback": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} From 56fbae728d7cd5a47eebafa44da5d8c4e8a679c1 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 24 Oct 2016 15:22:09 -0400 Subject: [PATCH 002/107] Add Spanner Admin Client --- composer.json | 8 +- phpunit.xml.dist | 2 + .../Admin/Database/V1/DatabaseAdminApi.php | 838 ++++++++++++ .../database_admin_client_config.json | 68 + .../Admin/Instance/V1/InstanceAdminApi.php | 1176 +++++++++++++++++ .../instance_admin_client_config.json | 78 ++ src/Spanner/Configuration.php | 163 +++ .../Connection/AdminConnectionInterface.php | 111 ++ src/Spanner/Connection/AdminGrpc.php | 263 ++++ .../Connection/ConnectionInterface.php | 61 + src/Spanner/Connection/Grpc.php | 119 ++ src/Spanner/Connection/IamDatabase.php | 54 + src/Spanner/Connection/IamInstance.php | 54 + src/Spanner/Database.php | 249 ++++ src/Spanner/Instance.php | 388 ++++++ src/Spanner/SpannerClient.php | 222 ++++ src/Spanner/V1/SpannerApi.php | 967 ++++++++++++++ .../V1/resources/spanner_client_config.json | 78 ++ tests/Spanner/ConfigurationTest.php | 123 ++ tests/Spanner/Connection/IamDatabaseTest.php | 95 ++ tests/Spanner/Connection/IamInstanceTest.php | 95 ++ tests/Spanner/DatabaseTest.php | 155 +++ tests/Spanner/InstanceTest.php | 318 +++++ tests/Spanner/SpannerClientTest.php | 154 +++ tests/fixtures/spanner/instance.json | 7 + tests/unit/Datastore/OperationTest.php | 5 + 26 files changed, 5850 insertions(+), 1 deletion(-) create mode 100644 src/Spanner/Admin/Database/V1/DatabaseAdminApi.php create mode 100644 src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json create mode 100644 src/Spanner/Admin/Instance/V1/InstanceAdminApi.php create mode 100644 src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json create mode 100644 src/Spanner/Configuration.php create mode 100644 src/Spanner/Connection/AdminConnectionInterface.php create mode 100644 src/Spanner/Connection/AdminGrpc.php create mode 100644 src/Spanner/Connection/ConnectionInterface.php create mode 100644 src/Spanner/Connection/Grpc.php create mode 100644 src/Spanner/Connection/IamDatabase.php create mode 100644 src/Spanner/Connection/IamInstance.php create mode 100644 src/Spanner/Database.php create mode 100644 src/Spanner/Instance.php create mode 100644 src/Spanner/SpannerClient.php create mode 100644 src/Spanner/V1/SpannerApi.php create mode 100644 src/Spanner/V1/resources/spanner_client_config.json create mode 100644 tests/Spanner/ConfigurationTest.php create mode 100644 tests/Spanner/Connection/IamDatabaseTest.php create mode 100644 tests/Spanner/Connection/IamInstanceTest.php create mode 100644 tests/Spanner/DatabaseTest.php create mode 100644 tests/Spanner/InstanceTest.php create mode 100644 tests/Spanner/SpannerClientTest.php create mode 100644 tests/fixtures/spanner/instance.json diff --git a/composer.json b/composer.json index 5606dbc21c5b..fa2e65161dd8 100644 --- a/composer.json +++ b/composer.json @@ -75,5 +75,11 @@ }, "scripts": { "google-cloud": "dev/google-cloud" - } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/jdpedrie/proto-client-php-private" + } + ] } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 8afd82d909fa..23718b678755 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,6 +10,8 @@ src src/*/V[!a-zA-Z]* + src/*/*/V[!a-zA-Z]* + src/*/*/*/V[!a-zA-Z]* diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminApi.php b/src/Spanner/Admin/Database/V1/DatabaseAdminApi.php new file mode 100644 index 000000000000..ae6da1eb2f81 --- /dev/null +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminApi.php @@ -0,0 +1,838 @@ +listDatabases($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class DatabaseAdminApi +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + const _GAX_VERSION = '0.1.0'; + const _CODEGEN_NAME = 'GAPIC'; + const _CODEGEN_VERSION = '0.0.0'; + + private static $instanceNameTemplate; + private static $databaseNameTemplate; + + private $grpcCredentialsHelper; + private $databaseAdminStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + + /** + * Formats a string containing the fully-qualified path to represent + * a instance resource. + */ + public static function formatInstanceName($project, $instance) + { + return self::getInstanceNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a database resource. + */ + public static function formatDatabaseName($project, $instance, $database) + { + return self::getDatabaseNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseProjectFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseInstanceFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['instance']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a database resource. + */ + public static function parseProjectFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a database resource. + */ + public static function parseInstanceFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a database resource. + */ + public static function parseDatabaseFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['database']; + } + + private static function getInstanceNameTemplate() + { + if (self::$instanceNameTemplate == null) { + self::$instanceNameTemplate = new PathTemplate('projects/{project}/instances/{instance}'); + } + + return self::$instanceNameTemplate; + } + + private static function getDatabaseNameTemplate() + { + if (self::$databaseNameTemplate == null) { + self::$databaseNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}'); + } + + return self::$databaseNameTemplate; + } + + private static function getPageStreamingDescriptors() + { + $listDatabasesPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'databases', + ]); + + $pageStreamingDescriptors = [ + 'listDatabases' => $listDatabasesPageStreamingDescriptor, + ]; + + return $pageStreamingDescriptors; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'wrenchworks.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner Admin Database API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultScopes = [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ]; + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => $defaultScopes, + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => self::_GAX_VERSION, + 'credentialsLoader' => null, + ]; + $options = array_merge($defaultOptions, $options); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::_CODEGEN_NAME, + 'codeGenVersion' => self::_CODEGEN_VERSION, + 'gaxVersion' => self::_GAX_VERSION, + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'listDatabases' => $defaultDescriptors, + 'createDatabase' => $defaultDescriptors, + 'updateDatabase' => $defaultDescriptors, + 'dropDatabase' => $defaultDescriptors, + 'getDatabaseDDL' => $defaultDescriptors, + 'setIamPolicy' => $defaultDescriptors, + 'getIamPolicy' => $defaultDescriptors, + 'testIamPermissions' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/database_admin_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.admin.database.v1.DatabaseAdmin', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (!empty($options['sslCreds'])) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createDatabaseAdminStubFunction = function ($hostname, $opts) { + return new DatabaseAdminClient($hostname, $opts); + }; + $this->databaseAdminStub = $this->grpcCredentialsHelper->createStub( + $createDatabaseAdminStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Lists Cloud Spanner databases. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedName = DatabaseAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * foreach ($databaseAdminApi->listDatabases($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The project whose databases should be listed. Required. + * Values are of the form `projects//instances/`. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return Google\GAX\PagedListResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function listDatabases($name, $optionalArgs = []) + { + $request = new ListDatabasesRequest(); + $request->setName($name); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + + $mergedSettings = $this->defaultCallSettings['listDatabases']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'ListDatabases', + $mergedSettings, + $this->descriptors['listDatabases'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Creates a new Cloud Spanner database. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedName = DatabaseAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $createStatement = ""; + * $response = $databaseAdminApi->createDatabase($formattedName, $createStatement); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the instance that will serve the new database. + * Values are of the form `projects//instances/`. + * @param string $createStatement A `CREATE DATABASE` statement, which specifies the name of the + * new database. The database name must conform to the regular expression + * `[a-z][a-z0-9_\-]*[a-z0-9]` and be between 2 and 30 characters in length. + * @param array $optionalArgs { + * Optional. + * + * @type string[] $extraStatements + * An optional list of DDL statements to run inside the newly created + * database. Statements can create tables, indexes, etc. These + * statements execute atomically with the creation of the database: + * if there is an error in any statement, the database is not created. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\admin\database\v1\Database + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function createDatabase($name, $createStatement, $optionalArgs = []) + { + $request = new CreateDatabaseRequest(); + $request->setName($name); + $request->setCreateStatement($createStatement); + if (isset($optionalArgs['extraStatements'])) { + foreach ($optionalArgs['extraStatements'] as $elem) { + $request->addExtraStatements($elem); + } + } + + $mergedSettings = $this->defaultCallSettings['createDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'CreateDatabase', + $mergedSettings, + $this->descriptors['createDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Updates the schema of a Cloud Spanner database by + * creating/altering/dropping tables, columns, indexes, etc. The + * [UpdateDatabaseMetadata][google.spanner.admin.database.v1.UpdateDatabaseMetadata] message is used for operation + * metadata; The operation has no response. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $statements = []; + * $response = $databaseAdminApi->updateDatabase($formattedDatabase, $statements); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $database The database to update. + * @param string[] $statements DDL statements to be applied to the database. + * @param array $optionalArgs { + * Optional. + * + * @type string $operationId + * If empty, the new update request is assigned an + * automatically-generated operation ID. Otherwise, `operation_id` + * is used to construct the name of the resulting + * [Operation][google.longrunning.Operation]. + * + * Specifying an explicit operation ID simplifies determining + * whether the statements were executed in the event that the + * [UpdateDatabase][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase] call is replayed, + * or the return value is otherwise lost: the [database][google.spanner.admin.database.v1.UpdateDatabaseRequest.database] and + * `operation_id` fields can be combined to form the + * [name][google.longrunning.Operation.name] of the resulting + * [longrunning.Operation][google.longrunning.Operation]: `/operations/`. + * + * `operation_id` should be unique within the database, and must be + * a valid identifier: `[a-zA-Z][a-zA-Z0-9_]*`. Note that + * automatically-generated operation IDs always begin with an + * underscore. If the named operation already exists, + * [UpdateDatabase][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase] returns + * `ALREADY_EXISTS`. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\longrunning\Operation + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function updateDatabase($database, $statements, $optionalArgs = []) + { + $request = new UpdateDatabaseRequest(); + $request->setDatabase($database); + foreach ($statements as $elem) { + $request->addStatements($elem); + } + if (isset($optionalArgs['operationId'])) { + $request->setOperationId($optionalArgs['operationId']); + } + + $mergedSettings = $this->defaultCallSettings['updateDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'UpdateDatabase', + $mergedSettings, + $this->descriptors['updateDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Drops (aka deletes) a Cloud Spanner database. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $databaseAdminApi->dropDatabase($formattedDatabase); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $database The database to be dropped. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function dropDatabase($database, $optionalArgs = []) + { + $request = new DropDatabaseRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['dropDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'DropDatabase', + $mergedSettings, + $this->descriptors['dropDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns the schema of a Cloud Spanner database as a list of formatted + * DDL statements. This method does not show pending schema updates, those may + * be queried using the [Operations][google.longrunning.Operations] API. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminApi->getDatabaseDDL($formattedDatabase); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $database The database whose schema we wish to get. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\admin\database\v1\GetDatabaseDDLResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getDatabaseDDL($database, $optionalArgs = []) + { + $request = new GetDatabaseDDLRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['getDatabaseDDL']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'GetDatabaseDDL', + $mergedSettings, + $this->descriptors['getDatabaseDDL'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Sets the access control policy on a database resource. Replaces any + * existing policy. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $policy = new Policy(); + * $response = $databaseAdminApi->setIamPolicy($formattedResource, $policy); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being specified. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param Policy $policy REQUIRED: The complete policy to be applied to the `resource`. The size of + * the policy is limited to a few 10s of KB. An empty policy is a + * valid policy but certain Cloud Platform services (such as Projects) + * might reject them. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\Policy + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function setIamPolicy($resource, $policy, $optionalArgs = []) + { + $request = new SetIamPolicyRequest(); + $request->setResource($resource); + $request->setPolicy($policy); + + $mergedSettings = $this->defaultCallSettings['setIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'SetIamPolicy', + $mergedSettings, + $this->descriptors['setIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets the access control policy for a database resource. Returns an empty + * policy if a database exists but does not have a policy set. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminApi->getIamPolicy($formattedResource); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\Policy + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getIamPolicy($resource, $optionalArgs = []) + { + $request = new GetIamPolicyRequest(); + $request->setResource($resource); + + $mergedSettings = $this->defaultCallSettings['getIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'GetIamPolicy', + $mergedSettings, + $this->descriptors['getIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns permissions that the caller has on the specified database resource. + * + * Sample code: + * ``` + * try { + * $databaseAdminApi = new DatabaseAdminApi(); + * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $permissions = []; + * $response = $databaseAdminApi->testIamPermissions($formattedResource, $permissions); + * } finally { + * if (isset($databaseAdminApi)) { + * $databaseAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy detail is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see + * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\TestIamPermissionsResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function testIamPermissions($resource, $permissions, $optionalArgs = []) + { + $request = new TestIamPermissionsRequest(); + $request->setResource($resource); + foreach ($permissions as $elem) { + $request->addPermissions($elem); + } + + $mergedSettings = $this->defaultCallSettings['testIamPermissions']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'TestIamPermissions', + $mergedSettings, + $this->descriptors['testIamPermissions'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->databaseAdminStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json new file mode 100644 index 000000000000..b1a17f9f00c7 --- /dev/null +++ b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json @@ -0,0 +1,68 @@ +{ + "interfaces": { + "google.spanner.admin.database.v1.DatabaseAdmin": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "ListDatabases": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CreateDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "UpdateDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DropDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetDatabaseDDL": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "SetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "TestIamPermissions": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminApi.php b/src/Spanner/Admin/Instance/V1/InstanceAdminApi.php new file mode 100644 index 000000000000..f38aff05a78e --- /dev/null +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminApi.php @@ -0,0 +1,1176 @@ +listInstanceConfigs($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class InstanceAdminApi +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + const _GAX_VERSION = '0.1.0'; + const _CODEGEN_NAME = 'GAPIC'; + const _CODEGEN_VERSION = '0.0.0'; + + private static $projectNameTemplate; + private static $instanceConfigNameTemplate; + private static $instanceNameTemplate; + + private $grpcCredentialsHelper; + private $instanceAdminStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + + /** + * Formats a string containing the fully-qualified path to represent + * a project resource. + */ + public static function formatProjectName($project) + { + return self::getProjectNameTemplate()->render([ + 'project' => $project, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a instance_config resource. + */ + public static function formatInstanceConfigName($project, $instanceConfig) + { + return self::getInstanceConfigNameTemplate()->render([ + 'project' => $project, + 'instance_config' => $instanceConfig, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a instance resource. + */ + public static function formatInstanceName($project, $instance) + { + return self::getInstanceNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a project resource. + */ + public static function parseProjectFromProjectName($projectName) + { + return self::getProjectNameTemplate()->match($projectName)['project']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instanceConfig resource. + */ + public static function parseProjectFromInstanceConfigName($instanceConfigName) + { + return self::getInstanceConfigNameTemplate()->match($instanceConfigName)['project']; + } + + /** + * Parses the instance_config from the given fully-qualified path which + * represents a instanceConfig resource. + */ + public static function parseInstanceConfigFromInstanceConfigName($instanceConfigName) + { + return self::getInstanceConfigNameTemplate()->match($instanceConfigName)['instance_config']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseProjectFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a instance resource. + */ + public static function parseInstanceFromInstanceName($instanceName) + { + return self::getInstanceNameTemplate()->match($instanceName)['instance']; + } + + private static function getProjectNameTemplate() + { + if (self::$projectNameTemplate == null) { + self::$projectNameTemplate = new PathTemplate('projects/{project}'); + } + + return self::$projectNameTemplate; + } + + private static function getInstanceConfigNameTemplate() + { + if (self::$instanceConfigNameTemplate == null) { + self::$instanceConfigNameTemplate = new PathTemplate('projects/{project}/instanceConfigs/{instance_config}'); + } + + return self::$instanceConfigNameTemplate; + } + + private static function getInstanceNameTemplate() + { + if (self::$instanceNameTemplate == null) { + self::$instanceNameTemplate = new PathTemplate('projects/{project}/instances/{instance}'); + } + + return self::$instanceNameTemplate; + } + + private static function getPageStreamingDescriptors() + { + $listInstanceConfigsPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'instance_configs', + ]); + $listInstancesPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'instances', + ]); + + $pageStreamingDescriptors = [ + 'listInstanceConfigs' => $listInstanceConfigsPageStreamingDescriptor, + 'listInstances' => $listInstancesPageStreamingDescriptor, + ]; + + return $pageStreamingDescriptors; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'wrenchworks.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner Admin Instance API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultScopes = [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ]; + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => $defaultScopes, + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => self::_GAX_VERSION, + 'credentialsLoader' => null, + ]; + $options = array_merge($defaultOptions, $options); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::_CODEGEN_NAME, + 'codeGenVersion' => self::_CODEGEN_VERSION, + 'gaxVersion' => self::_GAX_VERSION, + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'listInstanceConfigs' => $defaultDescriptors, + 'getInstanceConfig' => $defaultDescriptors, + 'listInstances' => $defaultDescriptors, + 'getInstance' => $defaultDescriptors, + 'createInstance' => $defaultDescriptors, + 'updateInstance' => $defaultDescriptors, + 'deleteInstance' => $defaultDescriptors, + 'setIamPolicy' => $defaultDescriptors, + 'getIamPolicy' => $defaultDescriptors, + 'testIamPermissions' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/instance_admin_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.admin.instance.v1.InstanceAdmin', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (!empty($options['sslCreds'])) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createInstanceAdminStubFunction = function ($hostname, $opts) { + return new InstanceAdminClient($hostname, $opts); + }; + $this->instanceAdminStub = $this->grpcCredentialsHelper->createStub( + $createInstanceAdminStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Lists the supported instance configurations for a given project. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatProjectName("[PROJECT]"); + * foreach ($instanceAdminApi->listInstanceConfigs($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the project for which a list of supported instance + * configurations is requested. Values are of the form + * `projects/`. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return Google\GAX\PagedListResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function listInstanceConfigs($name, $optionalArgs = []) + { + $request = new ListInstanceConfigsRequest(); + $request->setName($name); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + + $mergedSettings = $this->defaultCallSettings['listInstanceConfigs']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'ListInstanceConfigs', + $mergedSettings, + $this->descriptors['listInstanceConfigs'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets information about a particular instance configuration. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceConfigName("[PROJECT]", "[INSTANCE_CONFIG]"); + * $response = $instanceAdminApi->getInstanceConfig($formattedName); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the requested instance configuration. Values are of the form + * `projects//instanceConfigs/`. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\admin\instance\v1\InstanceConfig + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getInstanceConfig($name, $optionalArgs = []) + { + $request = new GetInstanceConfigRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getInstanceConfig']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetInstanceConfig', + $mergedSettings, + $this->descriptors['getInstanceConfig'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Lists all instances in the given project. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatProjectName("[PROJECT]"); + * foreach ($instanceAdminApi->listInstances($formattedName) as $element) { + * // doThingsWith(element); + * } + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name 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 is howl. + * * name:HOWL --> Equivalent to above. + * * NAME:howl --> Equivalent to above. + * * labels.env:* --> The instance has the label env. + * * labels.env:dev --> The instance's label env has the value dev. + * * name:howl labels.env:dev --> The instance's name is howl and it has + * the label env with value dev. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return Google\GAX\PagedListResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function listInstances($name, $optionalArgs = []) + { + $request = new ListInstancesRequest(); + $request->setName($name); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + if (isset($optionalArgs['filter'])) { + $request->setFilter($optionalArgs['filter']); + } + + $mergedSettings = $this->defaultCallSettings['listInstances']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'ListInstances', + $mergedSettings, + $this->descriptors['listInstances'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets information about a particular instance. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $response = $instanceAdminApi->getInstance($formattedName); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the requested instance. Values are of the form + * `projects//instances/`. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\admin\instance\v1\Instance + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getInstance($name, $optionalArgs = []) + { + $request = new GetInstanceRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetInstance', + $mergedSettings, + $this->descriptors['getInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Creates an instance and begins preparing it to begin serving. The + * returned [long-running operation][google.longrunning.Operation] + * can be used to track the progress of preparing the new + * instance. The instance name is assigned by the caller. If the + * named instance already exists, `CreateInstance` returns + * `ALREADY_EXISTS`. + * + * Immediately upon completion of this request: + * + * * The instance is readable via the API, with all requested attributes + * but no allocated resources. Its state is `CREATING`. + * + * Until completion of the returned operation: + * + * * Cancelling the operation renders the instance immediately unreadable + * via the API. + * * The instance can be deleted. + * * All other attempts to modify the instance are rejected. + * + * Upon completion of the returned operation: + * + * * Billing for all successfully-allocated resources begins (some types + * may have lower than the requested levels). + * * Databases can be created in the instance. + * * The instance's allocated resource levels are readable via the API. + * * The instance's state becomes `READY`. + * + * The returned operation's + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata] + * The returned operation's + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if + * successful. + * + * Authorization requires `spanner.instances.create` permission on + * resource [name][google.spanner.admin.instance.v1.Instance.name]. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $config = ""; + * $displayName = ""; + * $nodeCount = 0; + * $response = $instanceAdminApi->createInstance($formattedName, $config, $displayName, $nodeCount); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name A unique identifier for the instance, which cannot be changed after + * the instance is created. Values are of the form + * `projects//instances/[a-z][-a-z0-9]*[a-z0-9]`. The final + * segment of the name must be between 6 and 30 characters in length. + * @param string $config The name of the instance's configuration. Values are of the form + * `projects//instanceConfigs/`. See + * also [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig] and + * [ListInstanceConfigs][google.spanner.admin.instance.v1.InstanceAdmin.ListInstanceConfigs]. + * @param string $displayName The descriptive name for this instance as it appears in UIs. + * Must be unique per project and between 4 and 30 characters in length. + * @param int $nodeCount The number of nodes allocated to this instance. + * @param array $optionalArgs { + * Optional. + * + * @type State $state + * The current instance state. For + * [CreateInstance][google.spanner.admin.instance.v1.InstanceAdmin.CreateInstance], the state must be + * either omitted or set to `CREATING`. For + * [UpdateInstance][google.spanner.admin.instance.v1.InstanceAdmin.UpdateInstance], the state must be + * either omitted or set to `READY`. + * @type array $labels + * Cloud Labels are a flexible and lightweight mechanism for organizing cloud + * resources into groups that reflect a customer's organizational needs and + * deployment strategies. Cloud Labels can be used to filter collections of + * resources. They can be used to control how resource metrics are aggregated. + * And they can be used as arguments to policy management rules (e.g. route, + * firewall, load balancing, etc.). + * + * * Label keys must be between 1 and 63 characters long and must conform to + * the following regular expression: `[a-z]([-a-z0-9]*[a-z0-9])?`. + * * Label values must be between 0 and 63 characters long and must conform + * to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. + * * No more than 64 labels can be associated with a given resource. + * + * See https://goo.gl/xmQnxf for more information on and examples of labels. + * + * If you plan to use labels in your own code, please note that additional + * characters may be allowed in the future. And so you are advised to use an + * internal label representation, such as JSON, which doesn't rely upon + * specific characters being disallowed. For example, representing labels + * as the string: name + "_" + value would prove problematic if we were to + * allow "_" in a future release. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\longrunning\Operation + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function createInstance($name, $config, $displayName, $nodeCount, $optionalArgs = []) + { + $request = new Instance(); + $request->setName($name); + $request->setConfig($config); + $request->setDisplayName($displayName); + $request->setNodeCount($nodeCount); + if (isset($optionalArgs['state'])) { + $request->setState($optionalArgs['state']); + } + if (isset($optionalArgs['labels'])) { + foreach ($optionalArgs['labels'] as $key => $value) { + $request->addLabels((new LabelsEntry())->setKey($key)->setValue($value)); + } + } + + $mergedSettings = $this->defaultCallSettings['createInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'CreateInstance', + $mergedSettings, + $this->descriptors['createInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Updates an instance, and begins allocating or releasing resources + * as requested. The returned [long-running + * operation][google.longrunning.Operation] can be used to track the + * progress of updating the instance. If the named instance does not + * exist, returns `NOT_FOUND`. + * + * Immediately upon completion of this request: + * + * * For resource types for which a decrease in the instance's allocation + * has been requested, billing is based on the newly-requested level. + * + * Until completion of the returned operation: + * + * * Cancelling the operation sets its metadata's + * [cancel_time][google.spanner.admin.instance.v1.UpdateInstanceMetadata.cancel_time], and begins + * restoring resources to their pre-request values. The operation + * is guaranteed to succeed at undoing all resource changes, + * after which point it terminates with a `CANCELLED` status. + * * All other attempts to modify the instance are rejected. + * * Reading the instance via the API continues to give the pre-request + * resource levels. + * + * Upon completion of the returned operation: + * + * * Billing begins for all successfully-allocated resources (some types + * may have lower than the requested levels). + * * All newly-reserved resources are available for serving the instance's + * tables. + * * The instance's new resource levels are readable via the API. + * + * The returned operation's + * [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata] + * The returned operation's + * [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if + * successful. + * + * Authorization requires `spanner.instances.update` permission on + * resource [name][google.spanner.admin.instance.v1.Instance.name]. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $config = ""; + * $displayName = ""; + * $nodeCount = 0; + * $response = $instanceAdminApi->updateInstance($formattedName, $config, $displayName, $nodeCount); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name A unique identifier for the instance, which cannot be changed after + * the instance is created. Values are of the form + * `projects//instances/[a-z][-a-z0-9]*[a-z0-9]`. The final + * segment of the name must be between 6 and 30 characters in length. + * @param string $config The name of the instance's configuration. Values are of the form + * `projects//instanceConfigs/`. See + * also [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig] and + * [ListInstanceConfigs][google.spanner.admin.instance.v1.InstanceAdmin.ListInstanceConfigs]. + * @param string $displayName The descriptive name for this instance as it appears in UIs. + * Must be unique per project and between 4 and 30 characters in length. + * @param int $nodeCount The number of nodes allocated to this instance. + * @param array $optionalArgs { + * Optional. + * + * @type State $state + * The current instance state. For + * [CreateInstance][google.spanner.admin.instance.v1.InstanceAdmin.CreateInstance], the state must be + * either omitted or set to `CREATING`. For + * [UpdateInstance][google.spanner.admin.instance.v1.InstanceAdmin.UpdateInstance], the state must be + * either omitted or set to `READY`. + * @type array $labels + * Cloud Labels are a flexible and lightweight mechanism for organizing cloud + * resources into groups that reflect a customer's organizational needs and + * deployment strategies. Cloud Labels can be used to filter collections of + * resources. They can be used to control how resource metrics are aggregated. + * And they can be used as arguments to policy management rules (e.g. route, + * firewall, load balancing, etc.). + * + * * Label keys must be between 1 and 63 characters long and must conform to + * the following regular expression: `[a-z]([-a-z0-9]*[a-z0-9])?`. + * * Label values must be between 0 and 63 characters long and must conform + * to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. + * * No more than 64 labels can be associated with a given resource. + * + * See https://goo.gl/xmQnxf for more information on and examples of labels. + * + * If you plan to use labels in your own code, please note that additional + * characters may be allowed in the future. And so you are advised to use an + * internal label representation, such as JSON, which doesn't rely upon + * specific characters being disallowed. For example, representing labels + * as the string: name + "_" + value would prove problematic if we were to + * allow "_" in a future release. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\longrunning\Operation + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function updateInstance($name, $config, $displayName, $nodeCount, $optionalArgs = []) + { + $request = new Instance(); + $request->setName($name); + $request->setConfig($config); + $request->setDisplayName($displayName); + $request->setNodeCount($nodeCount); + if (isset($optionalArgs['state'])) { + $request->setState($optionalArgs['state']); + } + if (isset($optionalArgs['labels'])) { + foreach ($optionalArgs['labels'] as $key => $value) { + $request->addLabels((new LabelsEntry())->setKey($key)->setValue($value)); + } + } + + $mergedSettings = $this->defaultCallSettings['updateInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'UpdateInstance', + $mergedSettings, + $this->descriptors['updateInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Deletes an instance. + * + * Immediately upon completion of the request: + * + * * Billing ceases for all of the instance's reserved resources. + * + * Soon afterward: + * + * * The instance and *all of its databases* immediately and + * irrevocably disappear from the API. All data in the databases + * is permanently deleted. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $instanceAdminApi->deleteInstance($formattedName); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the instance to be deleted. Values are of the form + * `projects//instances/` + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function deleteInstance($name, $optionalArgs = []) + { + $request = new DeleteInstanceRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['deleteInstance']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'DeleteInstance', + $mergedSettings, + $this->descriptors['deleteInstance'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Sets the access control policy on an instance resource. Replaces any + * existing policy. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $policy = new Policy(); + * $response = $instanceAdminApi->setIamPolicy($formattedResource, $policy); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being specified. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param Policy $policy REQUIRED: The complete policy to be applied to the `resource`. The size of + * the policy is limited to a few 10s of KB. An empty policy is a + * valid policy but certain Cloud Platform services (such as Projects) + * might reject them. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\Policy + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function setIamPolicy($resource, $policy, $optionalArgs = []) + { + $request = new SetIamPolicyRequest(); + $request->setResource($resource); + $request->setPolicy($policy); + + $mergedSettings = $this->defaultCallSettings['setIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'SetIamPolicy', + $mergedSettings, + $this->descriptors['setIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets the access control policy for an instance resource. Returns an empty + * policy if an instance exists but does not have a policy set. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $response = $instanceAdminApi->getIamPolicy($formattedResource); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\Policy + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getIamPolicy($resource, $optionalArgs = []) + { + $request = new GetIamPolicyRequest(); + $request->setResource($resource); + + $mergedSettings = $this->defaultCallSettings['getIamPolicy']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'GetIamPolicy', + $mergedSettings, + $this->descriptors['getIamPolicy'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Returns permissions that the caller has on the specified instance resource. + * + * Sample code: + * ``` + * try { + * $instanceAdminApi = new InstanceAdminApi(); + * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $permissions = []; + * $response = $instanceAdminApi->testIamPermissions($formattedResource, $permissions); + * } finally { + * if (isset($instanceAdminApi)) { + * $instanceAdminApi->close(); + * } + * } + * ``` + * + * @param string $resource REQUIRED: The resource for which the policy detail is being requested. + * `resource` is usually specified as a path. For example, a Project + * resource is specified as `projects/{project}`. + * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with + * wildcards (such as '*' or 'storage.*') are not allowed. For more + * information see + * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\iam\v1\TestIamPermissionsResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function testIamPermissions($resource, $permissions, $optionalArgs = []) + { + $request = new TestIamPermissionsRequest(); + $request->setResource($resource); + foreach ($permissions as $elem) { + $request->addPermissions($elem); + } + + $mergedSettings = $this->defaultCallSettings['testIamPermissions']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->instanceAdminStub, + 'TestIamPermissions', + $mergedSettings, + $this->descriptors['testIamPermissions'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->instanceAdminStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json new file mode 100644 index 000000000000..2aa03b47e61d --- /dev/null +++ b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json @@ -0,0 +1,78 @@ +{ + "interfaces": { + "google.spanner.admin.instance.v1.InstanceAdmin": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "ListInstanceConfigs": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetInstanceConfig": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "ListInstances": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "GetInstance": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CreateInstance": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "UpdateInstance": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "DeleteInstance": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "SetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetIamPolicy": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "TestIamPermissions": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php new file mode 100644 index 000000000000..f95abd3fcc11 --- /dev/null +++ b/src/Spanner/Configuration.php @@ -0,0 +1,163 @@ +spanner(); + * + * $configuration = $spanner->configuration('regional-europe-west'); + * ``` + */ +class Configuration +{ + /** + * @var AdminConnectionInterface + */ + private $adminConnection; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var array + */ + private $info; + + /** + * Create a configuration instance. + * + * @param AdminConnectionInterface $adminConnection A service connection for the Spanner Admin API. + * @param string $projectId The current project ID. + * @param string $name The simple configuration name. + * @param array $info [optional] A service representation of the configuration. + */ + public function __construct( + AdminConnectionInterface $adminConnection, + $projectId, + $name, + array $info = [] + ) { + $this->adminConnection = $adminConnection; + $this->projectId = $projectId; + $this->name = $name; + $this->info = $info; + } + + /** + * Return the configuration name. + * + * Example: + * ``` + * $name = $configuration->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Return the service representation of the configuration. + * + * This method may require a service call. + * + * Example: + * ``` + * $info = $configuration->info(); + * echo $info['nodeCount']; + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function info(array $options = []) + { + if (!$this->info) { + $this->reload($options); + } + + return $this->info; + } + + /** + * Check if the configuration exists. + * + * This method requires a service call. + * + * Example: + * ``` + * if ($configuration->exists()) { + * echo 'The configuration exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function exists(array $options = []) + { + try { + $this->reload($options = []); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Fetch a fresh representation of the configuration from the service. + * + * Example: + * ``` + * $info = $configuration->reload(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function reload(array $options = []) + { + $this->info = $this->adminConnection->getConfig($options + [ + 'name' => InstanceAdminApi::formatInstanceConfigName($this->projectId, $this->name), + 'projectId' => $this->projectId + ]); + + return $this->info; + } +} diff --git a/src/Spanner/Connection/AdminConnectionInterface.php b/src/Spanner/Connection/AdminConnectionInterface.php new file mode 100644 index 000000000000..8039c3244039 --- /dev/null +++ b/src/Spanner/Connection/AdminConnectionInterface.php @@ -0,0 +1,111 @@ + CredentialsLoader::makeCredentials($config['scopes'], $config['keyFile']) + ]; + + $this->wrapper = new GrpcRequestWrapper; + + $this->instanceAdminApi = new InstanceAdminApi($grpcConfig); + $this->databaseAdminApi = new DatabaseAdminApi($grpcConfig); + } + + /** + * @param array $args [optional] + */ + public function listConfigs(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'listInstanceConfigs'], [ + $args['projectId'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getConfig(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'getInstanceConfig'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function listInstances(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'listInstances'], [ + InstanceAdminApi::formatProjectName($args['projectId']), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getInstance(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'getInstance'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function createInstance(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'createInstance'], [ + $args['name'], + $args['config'], + $args['displayName'], + $args['nodeCount'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function updateInstance(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'updateInstance'], [ + $args['name'], + $args['config'], + $args['displayName'], + $args['nodeCount'], + new State, + $args['labels'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function deleteInstance(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'deleteInstance'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getInstanceIamPolicy(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'getIamPolicy'], [ + $args['resource'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function setInstanceIamPolicy(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'setIamPolicy'], [ + $args['resource'], + $args['policy'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function testInstanceIamPermissions(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'testIamPermissions'], [ + $args['resource'], + $args['permissions'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function listDatabases(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'listDatabases'], [ + $args['instance'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function createDatabase(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'createDatabase'], [ + $args['instance'], + $args['createStatement'], + $args['extraStatements'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function updateDatabase(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'updateDatabase'], [ + $args['name'], + $args['statements'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function dropDatabase(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'dropDatabase'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getDatabaseDDL(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'getDatabaseDDL'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getDatabaseIamPolicy(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'getIamPolicy'], [ + $args['resource'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function setDatabaseIamPolicy(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'setIamPolicy'], [ + $args['resource'], + $args['policy'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function testDatabaseIamPermissions(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'testIamPermissions'], [ + $args['resource'], + $args['permissions'], + $args + ]); + } +} diff --git a/src/Spanner/Connection/ConnectionInterface.php b/src/Spanner/Connection/ConnectionInterface.php new file mode 100644 index 000000000000..e6026877f625 --- /dev/null +++ b/src/Spanner/Connection/ConnectionInterface.php @@ -0,0 +1,61 @@ + CredentialsLoader::makeCredentials($config['scopes'], $config['keyFile']) + ]; + + $this->spannerApi = new SpannerApi($grpcConfig); + } + + /** + * @param array $args [optional] + */ + public function createSession(array $args = []) + { + return $this->spannerApi->createSession( + SpannerApi::formatDatabaseName($args['projectId'], $args['instance'], $args['database']), + $this->filteredArgs($args) + )->serialize(new PhpArray()); + } + + /** + * @param array $args [optional] + */ + public function getSession(array $args = []) + { + $sessionName = SpannerApi::formatSessionName( + $args['projectId'], + $args['instance'], + $args['database'], + $args['name'] + ); + + return $this->spannerApi->getSession( + $sessionName, + $this->filteredArgs($args) + )->serialize(new PhpArray()); + } + + /** + * @param array $args [optional] + */ + public function deleteSession(array $args = []) + { + $sessionName = SpannerApi::formatSessionName( + $args['projectId'], + $args['instance'], + $args['database'], + $args['name'] + ); + + return $this->spannerApi->deleteSession( + $sessionName, + $this->filteredArgs($args) + )->serialize(new PhpArray()); + } + + /** + * @param array $args [optional] + */ + public function executeSql(array $args = []) + {} + + /** + * @param array $args [optional] + */ + public function read(array $args = []) + {} + + /** + * @param array $args [optional] + */ + public function beginTransaction(array $args = []) + {} + + /** + * @param array $args [optional] + */ + public function commit(array $args = []) + {} + + /** + * @param array $args [optional] + */ + public function rollback(array $args = []) + {} +} diff --git a/src/Spanner/Connection/IamDatabase.php b/src/Spanner/Connection/IamDatabase.php new file mode 100644 index 000000000000..47356523787c --- /dev/null +++ b/src/Spanner/Connection/IamDatabase.php @@ -0,0 +1,54 @@ +adminConnection = $adminConnection; + } + + /** + * @param array $args + */ + public function getPolicy(array $args) + { + return $this->adminConnection->getDatabaseIamPolicy($args); + } + + /** + * @param array $args + */ + public function setPolicy(array $args) + { + return $this->adminConnection->setDatabaseIamPolicy($args); + } + + /** + * @param array $args + */ + public function testPermissions(array $args) + { + return $this->adminConnection->testDatabaseIamPermissions($args); + } +} diff --git a/src/Spanner/Connection/IamInstance.php b/src/Spanner/Connection/IamInstance.php new file mode 100644 index 000000000000..564ae854165d --- /dev/null +++ b/src/Spanner/Connection/IamInstance.php @@ -0,0 +1,54 @@ +adminConnection = $adminConnection; + } + + /** + * @param array $args + */ + public function getPolicy(array $args) + { + return $this->adminConnection->getInstanceIamPolicy($args); + } + + /** + * @param array $args + */ + public function setPolicy(array $args) + { + return $this->adminConnection->setInstanceIamPolicy($args); + } + + /** + * @param array $args + */ + public function testPermissions(array $args) + { + return $this->adminConnection->testInstanceIamPermissions($args); + } +} diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php new file mode 100644 index 000000000000..3ae2d0809150 --- /dev/null +++ b/src/Spanner/Database.php @@ -0,0 +1,249 @@ +database('my-database'); + * ``` + */ +class Database +{ + /** + * @var AdminConnectionInterface + */ + private $adminConnection; + + /** + * @var Instance + */ + private $instance; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var Iam + */ + private $iam; + + /** + * Create an object representing a Database. + * + * @param AdminConnectionInterface $adminConnection The connection to the + * Google Cloud Spanner Admin API. + * @param Instance $instance The instance in which the database exists. + * @param string $projectId The project ID. + * @param string $name The database name. + * @param array $info [optional] A representation of the database object. + */ + public function __construct( + AdminConnectionInterface $adminConnection, + Instance $instance, + $projectId, + $name + ) { + $this->adminConnection = $adminConnection; + $this->instance = $instance; + $this->projectId = $projectId; + $this->name = $name; + $this->iam = new Iam( + new IamDatabase($this->adminConnection), + $this->fullyQualifiedDatabaseName() + ); + } + + /** + * Return the simple database name. + * + * Example: + * ``` + * $name = $database->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Check if the database exists. + * + * This method sends a service request. + * + * Example: + * ``` + * if ($database->exists()) { + * echo 'The database exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return bool + */ + public function exists(array $options = []) + { + try { + $this->adminConnection->getDatabaseDDL($options + [ + 'name' => $this->fullyQualifiedDatabaseName() + ]); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Update the Database. + * + * Example: + * ``` + * $database->update([ + * 'CREATE TABLE Users ( + * id INT64 NOT NULL, + * name STRING(100) NOT NULL + * password STRING(100) NOT NULL + * )' + * ]); + * ``` + * + * @param string|array $statements One or more DDL statements to execute. + * @param array $options [optional] Configuration options. + * @return + */ + public function update($statements, array $options = []) + { + $options += [ + 'operationId' => null + ]; + + if (!is_array($statements)) { + $statements = [$statements]; + } + + return $this->adminConnection->updateDatabase($options + [ + 'name' => $this->fullyQualifiedDatabaseName(), + 'statements' => $statements, + ]); + } + + /** + * Drop the database. + * + * Example: + * ``` + * $database->drop(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return void + */ + public function drop(array $options = []) + { + return $this->adminConnection->dropDatabase($options + [ + 'name' => $this->fullyQualifiedDatabaseName() + ]); + } + + /** + * Get a list of all database DDL statements. + * + * Example: + * ``` + * $statements = $database->ddl(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function ddl(array $options = []) + { + $ddl = $this->adminConnection->getDatabaseDDL($options + [ + 'name' => $this->fullyQualifiedDatabaseName() + ]); + + if (isset($ddl['statements'])) { + return $ddl['statements']; + } + + return []; + } + + /** + * Manage the database IAM policy + * + * Example: + * ``` + * $iam = $database->iam(); + * ``` + * + * @return Iam + */ + public function iam() + { + return $this->iam; + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'adminConnection' => get_class($this->adminConnection), + 'projectId' => $this->projectId, + 'name' => $this->name + ]; + } + + /** + * Convert the simple database name to a fully qualified name. + * + * @return string + */ + private function fullyQualifiedDatabaseName() + { + return DatabaseAdminApi::formatDatabaseName( + $this->projectId, + $this->instance->name(), + $this->name + ); + } +} diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php new file mode 100644 index 000000000000..ab4da66cca20 --- /dev/null +++ b/src/Spanner/Instance.php @@ -0,0 +1,388 @@ +instance('my-instance'); + * ``` + */ +class Instance +{ + const STATE_READY = State::READY; + const STATE_CREATING = State::CREATING; + + /** + * @var AdminConnectionInterface + */ + private $adminConnection; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var array + */ + private $info; + + /** + * @var Iam + */ + private $iam; + + /** + * Create an object representing a Google Cloud Spanner instance. + * + * @param AdminConnectionInterface $adminConnection The connection to the + * Google Cloud Spanner Admin API. + * @param string $projectId The project ID. + * @param string $name The instance name. + * @param array $info [optional] A representation of the instance object. + */ + public function __construct(AdminConnectionInterface $adminConnection, $projectId, $name, array $info = []) + { + $this->adminConnection = $adminConnection; + $this->projectId = $projectId; + $this->name = $name; + $this->info = $info; + $this->iam = new Iam( + new IamInstance($this->adminConnection), + $this->fullyQualifiedInstanceName() + ); + } + + /** + * Return the instance name. + * + * Example: + * ``` + * $name = $instance->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Return the service representation of the instance. + * + * This method may require a service call. + * + * Example: + * ``` + * $info = $instance->info(); + * echo $info['nodeCount']; + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function info(array $options = []) + { + if (!$this->info) { + $this->reload($options); + } + + return $this->info; + } + + /** + * Check if the instance exists. + * + * This method requires a service call. + * + * Example: + * ``` + * if ($instance->exists()) { + * echo 'The instance exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function exists(array $options = []) + { + try { + $this->reload($options = []); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Fetch a fresh representation of the instance from the service. + * + * Example: + * ``` + * $info = $instance->reload(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function reload(array $options = []) + { + $this->info = $this->adminConnection->getInstance($options + [ + 'name' => $this->fullyQualifiedInstanceName() + ]); + + return $this->info; + } + + /** + * Return the instance state. + * + * When instances are created or updated, they may take some time before + * they are ready for use. This method allows for checking whether an + * instance is ready. + * + * Example: + * ``` + * $instance = $spanner->createInstance($config, 'my-new-instance'); + * if ($instance->state() === Instance::STATE_READY) { + * // do stuff + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return string + */ + public function state(array $options = []) + { + $info = $this->info($options); + + return (isset($info['state'])) + ? $info['state'] + : null; + } + + /** + * Update the instance + * + * Example: + * ``` + * todo + * ``` + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1 Update Instance + * + * @param array $options { + * Configuration options + * + * @type Configuration $config The configuration to move the instante to. + * @return void + * @throws \InvalidArgumentException + */ + public function update(array $options = []) + { + $info = $this->info($options); + + $options += [ + 'displayName' => $info['displayName'], + 'nodeCount' => $info['nodeCount'], + 'config' => null, + 'labels' => (isset($info['labels'])) + ? $info['labels'] + : [] + ]; + + $config = $info['config']; + if ($options['config']) { + if (!($options['config'] instanceof Configuration)) { + throw new \InvalidArgumentException( + 'Given configuration is not an instance of Configuration.' + ); + } + + $config = InstanceAdminApi::formatInstanceConfigName( + $this->projectId, + $options['config']->name() + ); + } + + $this->adminConnection->updateInstance([ + 'name' => $this->fullyQualifiedInstanceName(), + 'config' => $config, + ] + $options); + } + + /** + * Delete the instance, any databases in the instance, and all data. + * + * Example: + * ``` + * $instance->delete(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return void + */ + public function delete(array $options = []) + { + return $this->adminConnection->deleteInstance($options + [ + 'name' => $this->fullyQualifiedInstanceName() + ]); + } + + /** + * Create a Database + * + * Example: + * ``` + * $database = $instance->createDatabase('my-database'); + * ``` + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/create Create Database + * + * @param string $name The database name. + * @param array $options [optional] { + * Configuration Options + * + * @type array $statements Additional DDL statements. + * } + * @return Database + */ + public function createDatabase($name, array $options = []) + { + $options += [ + 'statements' => [] + ]; + + $statement = sprintf('CREATE DATABASE `%s`', $name); + + $res = $this->adminConnection->createDatabase([ + 'instance' => $this->fullyQualifiedInstanceName(), + 'createStatement' => $statement, + 'extraStatements' => $options['statements'] + ]); + + return $this->database($name); + } + + /** + * Lazily instantiate a database object + * + * Example: + * ``` + * $database = $instance->database('my-database'); + * ``` + * + * @param string $name The database name + * @return Database + */ + public function database($name) + { + return new Database($this->adminConnection, $this, $this->projectId, $name); + } + + /** + * List databases in an instance + * + * Example: + * ``` + * $databases = $instance->databases(); + * ``` + * + * @todo implement pagination! + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/list List Databases + * + * @param array $options Configuration options. + * @return \Generator + */ + public function databases(array $options = []) + { + $res = $this->adminConnection->listDatabases($options + [ + 'instance' => $this->fullyQualifiedInstanceName(), + ]); + + $databases = []; + if (isset($res['databases'])) { + foreach ($res['databases'] as $database) { + yield $this->database( + DatabaseAdminApi::parseDatabaseFromDatabaseName($database['name']) + ); + } + } + } + + /** + * Manage the instance IAM policy + * + * Example: + * ``` + * $iam = $instance->iam(); + * ``` + * + * @return Iam + */ + public function iam() + { + return $this->iam; + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->adminConnection), + 'projectId' => $this->projectId, + 'name' => $this->name, + 'info' => $this->info + ]; + } + + /** + * Convert the simple instance name to a fully qualified name. + * + * @return string + */ + private function fullyQualifiedInstanceName() + { + return InstanceAdminApi::formatInstanceName($this->projectId, $this->name); + } +} diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php new file mode 100644 index 000000000000..f84550c2048a --- /dev/null +++ b/src/Spanner/SpannerClient.php @@ -0,0 +1,222 @@ +connection = new Grpc($this->configureAuthentication($config)); + $this->adminConnection = new AdminGrpc($this->configureAuthentication($config)); + } + + /** + * List all available configurations + * + * Example: + * ``` + * $configurations = $spanner->configurations(); + * ``` + * + * @todo implement pagination! + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs/list List Configs + * + * @return Generator + */ + public function configurations() + { + $res = $this->adminConnection->listConfigs([ + 'projectId' => InstanceAdminApi::formatProjectName($this->projectId) + ]); + + if (isset($res['instanceConfigs'])) { + foreach ($res['instanceConfigs'] as $config) { + $name = InstanceAdminApi::parseInstanceConfigFromInstanceConfigName($config['name']); + yield $this->configuration($name, $config); + } + } + } + + /** + * Get a configuration by its name + * + * Example: + * ``` + * $configuration = $spanner->configuration($configurationName); + * ``` + * + * @param string $name The Configuration name. + * @param array $config [optional] The configuration details. + * @return Configuration + */ + public function configuration($name, array $config = []) + { + return new Configuration($this->adminConnection, $this->projectId, $name, $config); + } + + /** + * Create an instance + * + * Example: + * ``` + * $instance = $spanner->createInstance($configuration, 'my-application-instance'); + * ``` + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/create Create Instance + * + * @codingStandardsIgnoreStart + * @param Configuration $config The configuration to use + * @param string $name The instance name + * @param array $options [optional] { + * Configuration options + * + * @type string $displayName **Defaults to** the value of $name. + * @type int $nodeCount **Defaults to** `1`. + * @type int $state **Defaults to** + * @type array $labels [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). + * } + * @return Instance + * @codingStandardsIgnoreEnd + */ + public function createInstance(Configuration $config, $name, array $options = []) + { + $options += [ + 'displayName' => $name, + 'nodeCount' => self::DEFAULT_NODE_COUNT, + 'state' => State::CREATING, + 'labels' => [] + ]; + + $res = $this->adminConnection->createInstance($options + [ + 'name' => InstanceAdminApi::formatInstanceName($this->projectId, $name), + 'config' => InstanceAdminApi::formatInstanceConfigName($this->projectId, $config->name()) + ]); + + return $this->instance($name); + } + + /** + * Lazily instantiate an instance + * + * Example: + * ``` + * $instance = $spanner->instance('my-application-instance'); + * ``` + * + * @param string $name The instance name + * @return Instance + */ + public function instance($name, array $instance = []) + { + return new Instance($this->adminConnection, $this->projectId, $name, $instance); + } + + /** + * List instances in the project + * + * Example: + * ``` + * $instances = $spanner->instances(); + * ``` + * + * @todo implement pagination! + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/list List Instances + * + * @param array $options [optional] Configuration options + * @return Generator + */ + public function instances(array $options = []) + { + $options += [ + 'filter' => null + ]; + + $res = $this->adminConnection->listInstances($options + [ + 'projectId' => $this->projectId, + ]); + + if (isset($res['instances'])) { + foreach ($res['instances'] as $instance) { + yield $this->instance( + InstanceAdminApi::parseInstanceFromInstanceName($instance['name']), + $instance + ); + } + } + } +} diff --git a/src/Spanner/V1/SpannerApi.php b/src/Spanner/V1/SpannerApi.php new file mode 100644 index 000000000000..4607be193f4d --- /dev/null +++ b/src/Spanner/V1/SpannerApi.php @@ -0,0 +1,967 @@ +createSession($formattedDatabase); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * Many parameters require resource names to be formatted in a particular way. To assist + * with these names, this class includes a format method for each type of name, and additionally + * a parse method to extract the individual identifiers contained within names that are + * returned. + */ +class SpannerApi +{ + /** + * The default address of the service. + */ + const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + + /** + * The default port of the service. + */ + const DEFAULT_SERVICE_PORT = 443; + + /** + * The default timeout for non-retrying methods. + */ + const DEFAULT_TIMEOUT_MILLIS = 30000; + + const _GAX_VERSION = '0.1.0'; + const _CODEGEN_NAME = 'GAPIC'; + const _CODEGEN_VERSION = '0.0.0'; + + private static $databaseNameTemplate; + private static $sessionNameTemplate; + + private $grpcCredentialsHelper; + private $spannerStub; + private $scopes; + private $defaultCallSettings; + private $descriptors; + + /** + * Formats a string containing the fully-qualified path to represent + * a database resource. + */ + public static function formatDatabaseName($project, $instance, $database) + { + return self::getDatabaseNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + ]); + } + + /** + * Formats a string containing the fully-qualified path to represent + * a session resource. + */ + public static function formatSessionName($project, $instance, $database, $session) + { + return self::getSessionNameTemplate()->render([ + 'project' => $project, + 'instance' => $instance, + 'database' => $database, + 'session' => $session, + ]); + } + + /** + * Parses the project from the given fully-qualified path which + * represents a database resource. + */ + public static function parseProjectFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a database resource. + */ + public static function parseInstanceFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a database resource. + */ + public static function parseDatabaseFromDatabaseName($databaseName) + { + return self::getDatabaseNameTemplate()->match($databaseName)['database']; + } + + /** + * Parses the project from the given fully-qualified path which + * represents a session resource. + */ + public static function parseProjectFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['project']; + } + + /** + * Parses the instance from the given fully-qualified path which + * represents a session resource. + */ + public static function parseInstanceFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['instance']; + } + + /** + * Parses the database from the given fully-qualified path which + * represents a session resource. + */ + public static function parseDatabaseFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['database']; + } + + /** + * Parses the session from the given fully-qualified path which + * represents a session resource. + */ + public static function parseSessionFromSessionName($sessionName) + { + return self::getSessionNameTemplate()->match($sessionName)['session']; + } + + private static function getDatabaseNameTemplate() + { + if (self::$databaseNameTemplate == null) { + self::$databaseNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}'); + } + + return self::$databaseNameTemplate; + } + + private static function getSessionNameTemplate() + { + if (self::$sessionNameTemplate == null) { + self::$sessionNameTemplate = new PathTemplate('projects/{project}/instances/{instance}/databases/{database}/sessions/{session}'); + } + + return self::$sessionNameTemplate; + } + + private static function getPageStreamingDescriptors() + { + $pageStreamingDescriptors = [ + ]; + + return $pageStreamingDescriptors; + } + + // TODO(garrettjones): add channel (when supported in gRPC) + /** + * Constructor. + * + * @param array $options { + * Optional. Options for configuring the service API wrapper. + * + * @type string $serviceAddress The domain name of the API remote host. + * Default 'wrenchworks.googleapis.com'. + * @type mixed $port The port on which to connect to the remote host. Default 443. + * @type Grpc\ChannelCredentials $sslCreds + * A `ChannelCredentials` for use with an SSL-enabled channel. + * Default: a credentials object returned from + * Grpc\ChannelCredentials::createSsl() + * @type array $scopes A string array of scopes to use when acquiring credentials. + * Default the scopes for the Google Cloud Spanner API. + * @type array $retryingOverride + * An associative array of string => RetryOptions, where the keys + * are method names (e.g. 'createFoo'), that overrides default retrying + * settings. A value of null indicates that the method in question should + * not retry. + * @type int $timeoutMillis The timeout in milliseconds to use for calls + * that don't use retries. For calls that use retries, + * set the timeout in RetryOptions. + * Default: 30000 (30 seconds) + * @type string $appName The codename of the calling service. Default 'gax'. + * @type string $appVersion The version of the calling service. + * Default: the current version of GAX. + * @type Google\Auth\CredentialsLoader $credentialsLoader + * A CredentialsLoader object created using the + * Google\Auth library. + * } + */ + public function __construct($options = []) + { + $defaultScopes = [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.data', + ]; + $defaultOptions = [ + 'serviceAddress' => self::SERVICE_ADDRESS, + 'port' => self::DEFAULT_SERVICE_PORT, + 'scopes' => $defaultScopes, + 'retryingOverride' => null, + 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, + 'appName' => 'gax', + 'appVersion' => self::_GAX_VERSION, + 'credentialsLoader' => null, + ]; + $options = array_merge($defaultOptions, $options); + + $headerDescriptor = new AgentHeaderDescriptor([ + 'clientName' => $options['appName'], + 'clientVersion' => $options['appVersion'], + 'codeGenName' => self::_CODEGEN_NAME, + 'codeGenVersion' => self::_CODEGEN_VERSION, + 'gaxVersion' => self::_GAX_VERSION, + 'phpVersion' => phpversion(), + ]); + + $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; + $this->descriptors = [ + 'createSession' => $defaultDescriptors, + 'getSession' => $defaultDescriptors, + 'deleteSession' => $defaultDescriptors, + 'executeSql' => $defaultDescriptors, + 'read' => $defaultDescriptors, + 'beginTransaction' => $defaultDescriptors, + 'commit' => $defaultDescriptors, + 'rollback' => $defaultDescriptors, + ]; + $pageStreamingDescriptors = self::getPageStreamingDescriptors(); + foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { + $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + } + + $clientConfigJsonString = file_get_contents(__DIR__.'/resources/spanner_client_config.json'); + $clientConfig = json_decode($clientConfigJsonString, true); + $this->defaultCallSettings = + CallSettings::load( + 'google.spanner.v1.Spanner', + $clientConfig, + $options['retryingOverride'], + GrpcConstants::getStatusCodeNames(), + $options['timeoutMillis'] + ); + + $this->scopes = $options['scopes']; + + $createStubOptions = []; + if (!empty($options['sslCreds'])) { + $createStubOptions['sslCreds'] = $options['sslCreds']; + } + $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); + $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); + + $createSpannerStubFunction = function ($hostname, $opts) { + return new SpannerClient($hostname, $opts); + }; + $this->spannerStub = $this->grpcCredentialsHelper->createStub( + $createSpannerStubFunction, + $options['serviceAddress'], + $options['port'], + $createStubOptions + ); + } + + /** + * Creates a new session. A session can be used to perform + * transactions that read and/or modify data in a Cloud Spanner database. + * Sessions are meant to be reused for many consecutive + * transactions. + * + * Sessions can only execute one transaction at a time. To execute + * multiple concurrent read-write/write-only transactions, create + * multiple sessions. Note that standalone reads and queries use a + * transaction internally, and count toward the one transaction + * limit. + * + * Cloud Spanner limits the number of sessions that can exist at any given + * time; thus, it is a good idea to delete idle and/or unneeded sessions. + * Aside from explicit deletes, Cloud Spanner can delete sessions for + * which no operations are sent for more than an hour, or due to + * internal errors. If a session is deleted, requests to it + * return `NOT_FOUND`. + * + * Idle sessions can be kept alive by sending a trivial SQL query + * periodically, e.g., `"SELECT 1"`. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedDatabase = SpannerApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $spannerApi->createSession($formattedDatabase); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $database The database in which the new session is created. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\Session + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function createSession($database, $optionalArgs = []) + { + $request = new CreateSessionRequest(); + $request->setDatabase($database); + + $mergedSettings = $this->defaultCallSettings['createSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'CreateSession', + $mergedSettings, + $this->descriptors['createSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Gets a session. Returns `NOT_FOUND` if the session does not exist. + * This is mainly useful for determining whether a session is still + * alive. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedName = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $response = $spannerApi->getSession($formattedName); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the session to retrieve. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\Session + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function getSession($name, $optionalArgs = []) + { + $request = new GetSessionRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'GetSession', + $mergedSettings, + $this->descriptors['getSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Ends a session, releasing server resources associated with it. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedName = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerApi->deleteSession($formattedName); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $name The name of the session to delete. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function deleteSession($name, $optionalArgs = []) + { + $request = new DeleteSessionRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['deleteSession']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'DeleteSession', + $mergedSettings, + $this->descriptors['deleteSession'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Executes an SQL query, returning all rows in a single reply. This + * method cannot be used to return a result set larger than 10 MiB; + * if the query yields more data than that, the query fails with + * a `FAILED_PRECONDITION` error. + * + * Queries inside read-write transactions might return `ABORTED`. If + * this occurs, the application should restart the transaction from + * the beginning. See [Transaction][google.spanner.v1.Transaction] for more details. + * + * Larger result sets can be fetched in streaming fashion by calling + * [ExecuteStreamingSql][google.spanner.v1.Spanner.ExecuteStreamingSql] instead. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $sql = ""; + * $response = $spannerApi->executeSql($formattedSession, $sql); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the SQL query should be performed. + * @param string $sql The SQL query string. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type Struct $params + * The SQL query string can contain parameter placeholders. A parameter + * placeholder consists of `'@'` followed by the parameter + * name. Parameter names consist of any combination of letters, + * numbers, and underscores. + * + * Parameters can appear anywhere that a literal value is expected. The same + * parameter name can be used more than once, for example: + * `"WHERE id > @msg_id AND id < @msg_id + 100"` + * + * It is an error to execute an SQL query with unbound parameters. + * + * Parameter values are specified using `params`, which is a JSON + * object whose keys are parameter names, and whose values are the + * corresponding parameter values. + * @type array $paramTypes + * It is not always possible for Cloud Spanner to infer the right SQL type + * from a JSON value. For example, values of type `BYTES` and values + * of type `STRING` both appear in [params][google.spanner.v1.ExecuteSqlRequest.params] as JSON strings. + * + * In these cases, `param_types` can be used to specify the exact + * SQL type for some or all of the SQL query parameters. See the + * definition of [Type][google.spanner.v1.Type] for more information + * about SQL types. + * @type string $resumeToken + * If this request is resuming a previously interrupted SQL query + * execution, `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new SQL query execution to resume where the last one left + * off. The rest of the request parameters must exactly match the + * request that yielded this token. + * @type QueryMode $queryMode + * Used to control the amount of debugging information returned in + * [ResultSetStats][google.spanner.v1.ResultSetStats]. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\ResultSet + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function executeSql($session, $sql, $optionalArgs = []) + { + $request = new ExecuteSqlRequest(); + $request->setSession($session); + $request->setSql($sql); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['params'])) { + $request->setParams($optionalArgs['params']); + } + if (isset($optionalArgs['paramTypes'])) { + foreach ($optionalArgs['paramTypes'] as $key => $value) { + $request->addParamTypes((new ParamTypesEntry())->setKey($key)->setValue($value)); + } + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + if (isset($optionalArgs['queryMode'])) { + $request->setQueryMode($optionalArgs['queryMode']); + } + + $mergedSettings = $this->defaultCallSettings['executeSql']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'ExecuteSql', + $mergedSettings, + $this->descriptors['executeSql'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Reads rows from the database using key lookups and scans, as a + * simple key/value style alternative to + * [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql]. This method cannot be used to + * return a result set larger than 10 MiB; if the read matches more + * data than that, the read fails with a `FAILED_PRECONDITION` + * error. + * + * Reads inside read-write transactions might return `ABORTED`. If + * this occurs, the application should restart the transaction from + * the beginning. See [Transaction][google.spanner.v1.Transaction] for more details. + * + * Larger result sets can be yielded in streaming fashion by calling + * [StreamingRead][google.spanner.v1.Spanner.StreamingRead] instead. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $table = ""; + * $columns = []; + * $keySet = new KeySet(); + * $response = $spannerApi->read($formattedSession, $table, $columns, $keySet); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the read should be performed. + * @param string $table The name of the table in the database to be read. Must be non-empty. + * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching + * this request. + * @param KeySet $keySet `key_set` identifies the rows to be yielded. `key_set` names the + * primary keys of the rows in [table][google.spanner.v1.ReadRequest.table] to be yielded, unless [index][google.spanner.v1.ReadRequest.index] + * is present. If [index][google.spanner.v1.ReadRequest.index] is present, then [key_set][google.spanner.v1.ReadRequest.key_set] instead names + * index keys in [index][google.spanner.v1.ReadRequest.index]. + * + * Rows are yielded in table primary key order (if [index][google.spanner.v1.ReadRequest.index] is empty) + * or index key order (if [index][google.spanner.v1.ReadRequest.index] is non-empty). + * + * It is not an error for the `key_set` to name rows that do not + * exist in the database. Read yields nothing for nonexistent rows. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type string $index + * If non-empty, the name of an index on [table][google.spanner.v1.ReadRequest.table]. This index is + * used instead of the table primary key when interpreting [key_set][google.spanner.v1.ReadRequest.key_set] + * and sorting result rows. See [key_set][google.spanner.v1.ReadRequest.key_set] for further information. + * @type int $offset + * The first `offset` rows matching [key_set][google.spanner.v1.ReadRequest.key_set] are skipped. Note + * that the implementation must read the rows in order to skip + * them. Where possible, it is much more efficient to adjust [key_set][google.spanner.v1.ReadRequest.key_set] + * to exclude unwanted rows. + * @type int $limit + * If greater than zero, after skipping the first [offset][google.spanner.v1.ReadRequest.offset] rows, + * only the next `limit` rows are yielded. If `limit` is zero, + * the default is no limit. + * @type string $resumeToken + * If this request is resuming a previously interrupted read, + * `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new read to resume where the last read left off. The + * rest of the request parameters must exactly match the request + * that yielded this token. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\ResultSet + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function read($session, $table, $columns, $keySet, $optionalArgs = []) + { + $request = new ReadRequest(); + $request->setSession($session); + $request->setTable($table); + foreach ($columns as $elem) { + $request->addColumns($elem); + } + $request->setKeySet($keySet); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['index'])) { + $request->setIndex($optionalArgs['index']); + } + if (isset($optionalArgs['offset'])) { + $request->setOffset($optionalArgs['offset']); + } + if (isset($optionalArgs['limit'])) { + $request->setLimit($optionalArgs['limit']); + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + + $mergedSettings = $this->defaultCallSettings['read']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Read', + $mergedSettings, + $this->descriptors['read'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Begins a new transaction. This step can often be skipped: + * [Read][google.spanner.v1.Spanner.Read], [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] and + * [Commit][google.spanner.v1.Spanner.Commit] can begin a new transaction as a + * side-effect. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $options = new TransactionOptions(); + * $response = $spannerApi->beginTransaction($formattedSession, $options); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the transaction runs. + * @param TransactionOptions $options Options for the new transaction. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\Transaction + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function beginTransaction($session, $options, $optionalArgs = []) + { + $request = new BeginTransactionRequest(); + $request->setSession($session); + $request->setOptions($options); + + $mergedSettings = $this->defaultCallSettings['beginTransaction']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'BeginTransaction', + $mergedSettings, + $this->descriptors['beginTransaction'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Commits a transaction. The request includes the mutations to be + * applied to rows in the database. + * + * `Commit` might return an `ABORTED` error. This can occur at any time; + * commonly, the cause is conflicts with concurrent + * transactions. However, it can also happen for a variety of other + * reasons. If `Commit` returns `ABORTED`, the caller should re-attempt + * the transaction from the beginning, re-using the same session. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $mutations = []; + * $response = $spannerApi->commit($formattedSession, $mutations); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the transaction to be committed is running. + * @param Mutation[] $mutations The mutations to be executed when this transaction commits. All + * mutations are applied atomically, in the order they appear in + * this list. + * @param array $optionalArgs { + * Optional. + * + * @type string $transactionId + * Commit a previously-started transaction. + * @type TransactionOptions $singleUseTransaction + * Execute mutations in a temporary transaction. Note that unlike + * commit of a previously-started transaction, commit with a + * temporary transaction is non-idempotent. That is, if the + * `CommitRequest` is sent to Cloud Spanner more than once (for + * instance, due to retries in the application, or in the + * transport library), it is possible that the mutations are + * executed more than once. If this is undesirable, use + * [BeginTransaction][google.spanner.v1.Spanner.BeginTransaction] and + * [Commit][google.spanner.v1.Spanner.Commit] instead. + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return google\spanner\v1\CommitResponse + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function commit($session, $mutations, $optionalArgs = []) + { + $request = new CommitRequest(); + $request->setSession($session); + foreach ($mutations as $elem) { + $request->addMutations($elem); + } + if (isset($optionalArgs['transactionId'])) { + $request->setTransactionId($optionalArgs['transactionId']); + } + if (isset($optionalArgs['singleUseTransaction'])) { + $request->setSingleUseTransaction($optionalArgs['singleUseTransaction']); + } + + $mergedSettings = $this->defaultCallSettings['commit']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Commit', + $mergedSettings, + $this->descriptors['commit'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Rolls back a transaction, releasing any locks it holds. It is a good + * idea to call this for any transaction that includes one or more + * [Read][google.spanner.v1.Spanner.Read] or [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] requests and + * ultimately decides not to commit. + * + * `Rollback` returns `OK` if it successfully aborts the transaction, the + * transaction was already aborted, or the transaction is not + * found. `Rollback` never returns `ABORTED`. + * + * Sample code: + * ``` + * try { + * $spannerApi = new SpannerApi(); + * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $transactionId = ""; + * $spannerApi->rollback($formattedSession, $transactionId); + * } finally { + * if (isset($spannerApi)) { + * $spannerApi->close(); + * } + * } + * ``` + * + * @param string $session The session in which the transaction to roll back is running. + * @param string $transactionId The transaction to roll back. + * @param array $optionalArgs { + * Optional. + * + * @type Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws Google\GAX\ApiException if the remote call fails + */ + public function rollback($session, $transactionId, $optionalArgs = []) + { + $request = new RollbackRequest(); + $request->setSession($session); + $request->setTransactionId($transactionId); + + $mergedSettings = $this->defaultCallSettings['rollback']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'Rollback', + $mergedSettings, + $this->descriptors['rollback'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Initiates an orderly shutdown in which preexisting calls continue but new + * calls are immediately cancelled. + */ + public function close() + { + $this->spannerStub->close(); + } + + private function createCredentialsCallback() + { + return $this->grpcCredentialsHelper->createCallCredentialsCallback(); + } +} diff --git a/src/Spanner/V1/resources/spanner_client_config.json b/src/Spanner/V1/resources/spanner_client_config.json new file mode 100644 index 000000000000..6299ccfa6961 --- /dev/null +++ b/src/Spanner/V1/resources/spanner_client_config.json @@ -0,0 +1,78 @@ +{ + "interfaces": { + "google.spanner.v1.Spanner": { + "retry_codes": { + "retry_codes_def": { + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [] + } + }, + "retry_params": { + "default": { + "initial_retry_delay_millis": 100, + "retry_delay_multiplier": 1.3, + "max_retry_delay_millis": 60000, + "initial_rpc_timeout_millis": 60000, + "rpc_timeout_multiplier": 1.0, + "max_rpc_timeout_millis": 60000, + "total_timeout_millis": 600000 + } + }, + "methods": { + "CreateSession": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "GetSession": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DeleteSession": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "ExecuteSql": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "ExecuteStreamingSql": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Read": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "StreamingRead": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "BeginTransaction": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Commit": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, + "Rollback": { + "timeout_millis": 30000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + } + } + } + } +} diff --git a/tests/Spanner/ConfigurationTest.php b/tests/Spanner/ConfigurationTest.php new file mode 100644 index 000000000000..29f6effb8881 --- /dev/null +++ b/tests/Spanner/ConfigurationTest.php @@ -0,0 +1,123 @@ +adminConnection = $this->prophesize(AdminConnectionInterface::class); + $this->configuration = new ConfigurationStub( + $this->adminConnection->reveal(), + self::PROJECT_ID, + self::NAME + ); + } + + public function testName() + { + $this->assertEquals(self::NAME, $this->configuration->name()); + } + + public function testInfo() + { + $this->adminConnection->getConfig(Argument::any())->shouldNotBeCalled(); + $this->configuration->setAdminConnection($this->adminConnection->reveal()); + + $info = ['foo' => 'bar']; + $config = new ConfigurationStub( + $this->adminConnection->reveal(), + self::PROJECT_ID, + self::NAME, + $info + ); + + $this->assertEquals($info, $config->info()); + } + + public function testInfoWithReload() + { + $info = ['foo' => 'bar']; + + $this->adminConnection->getConfig([ + 'name' => InstanceAdminApi::formatInstanceConfigName(self::PROJECT_ID, self::NAME), + 'projectId' => self::PROJECT_ID + ])->shouldBeCalled()->willReturn($info); + + $this->configuration->setAdminConnection($this->adminConnection->reveal()); + + $this->assertEquals($info, $this->configuration->info()); + } + + public function testExists() + { + $this->adminConnection->getConfig(Argument::any())->willReturn([]); + $this->configuration->setAdminConnection($this->adminConnection->reveal()); + + $this->assertTrue($this->configuration->exists()); + } + + public function testExistsDoesntExist() + { + $this->adminConnection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404)); + $this->configuration->setAdminConnection($this->adminConnection->reveal()); + + $this->assertFalse($this->configuration->exists()); + } + + public function testReload() + { + $info = ['foo' => 'bar']; + + $this->adminConnection->getConfig([ + 'name' => InstanceAdminApi::formatInstanceConfigName(self::PROJECT_ID, self::NAME), + 'projectId' => self::PROJECT_ID + ])->shouldBeCalledTimes(1)->willReturn($info); + + $this->configuration->setAdminConnection($this->adminConnection->reveal()); + + $info = $this->configuration->reload(); + + $info2 = $this->configuration->info(); + + $this->assertEquals($info, $info2); + } +} + +class ConfigurationStub extends Configuration +{ + public function setAdminConnection($conn) + { + $this->adminConnection = $conn; + } +} diff --git a/tests/Spanner/Connection/IamDatabaseTest.php b/tests/Spanner/Connection/IamDatabaseTest.php new file mode 100644 index 000000000000..870646654481 --- /dev/null +++ b/tests/Spanner/Connection/IamDatabaseTest.php @@ -0,0 +1,95 @@ +connection = $this->prophesize(AdminConnectionInterface::class); + + $this->iam = new IamDatabaseStub($this->connection->reveal()); + } + + public function testGetPolicy() + { + $args = ['key' => 'val']; + $res = ['foo' => 'bar']; + + $this->connection->getDatabaseIamPolicy(Argument::exact($args)) + ->shouldBeCalled() + ->willReturn($res); + + $this->iam->setConnection($this->connection->reveal()); + + $p = $this->iam->getPolicy($args); + + $this->assertEquals($res, $p); + } + + public function testSetPolicy() + { + $args = ['key' => 'val']; + $res = ['foo' => 'bar']; + + $this->connection->setDatabaseIamPolicy(Argument::exact($args)) + ->shouldBeCalled() + ->willReturn($res); + + $this->iam->setConnection($this->connection->reveal()); + + $p = $this->iam->setPolicy($args); + + $this->assertEquals($res, $p); + } + + public function testTestPermissions() + { + $args = ['key' => 'val']; + $res = ['foo' => 'bar']; + + $this->connection->testDatabaseIamPermissions(Argument::exact($args)) + ->shouldBeCalled() + ->willReturn($res); + + $this->iam->setConnection($this->connection->reveal()); + + $p = $this->iam->testPermissions($args); + + $this->assertEquals($res, $p); + } +} + +class IamDatabaseStub extends IamDatabase +{ + public function setConnection($conn) + { + $this->adminConnection = $conn; + } +} diff --git a/tests/Spanner/Connection/IamInstanceTest.php b/tests/Spanner/Connection/IamInstanceTest.php new file mode 100644 index 000000000000..77cb7d12ed4c --- /dev/null +++ b/tests/Spanner/Connection/IamInstanceTest.php @@ -0,0 +1,95 @@ +connection = $this->prophesize(AdminConnectionInterface::class); + + $this->iam = new IamInstanceStub($this->connection->reveal()); + } + + public function testGetPolicy() + { + $args = ['key' => 'val']; + $res = ['foo' => 'bar']; + + $this->connection->getInstanceIamPolicy(Argument::exact($args)) + ->shouldBeCalled() + ->willReturn($res); + + $this->iam->setConnection($this->connection->reveal()); + + $p = $this->iam->getPolicy($args); + + $this->assertEquals($res, $p); + } + + public function testSetPolicy() + { + $args = ['key' => 'val']; + $res = ['foo' => 'bar']; + + $this->connection->setInstanceIamPolicy(Argument::exact($args)) + ->shouldBeCalled() + ->willReturn($res); + + $this->iam->setConnection($this->connection->reveal()); + + $p = $this->iam->setPolicy($args); + + $this->assertEquals($res, $p); + } + + public function testTestPermissions() + { + $args = ['key' => 'val']; + $res = ['foo' => 'bar']; + + $this->connection->testInstanceIamPermissions(Argument::exact($args)) + ->shouldBeCalled() + ->willReturn($res); + + $this->iam->setConnection($this->connection->reveal()); + + $p = $this->iam->testPermissions($args); + + $this->assertEquals($res, $p); + } +} + +class IamInstanceStub extends IamInstance +{ + public function setConnection($conn) + { + $this->adminConnection = $conn; + } +} diff --git a/tests/Spanner/DatabaseTest.php b/tests/Spanner/DatabaseTest.php new file mode 100644 index 000000000000..ddf34ba8a5ec --- /dev/null +++ b/tests/Spanner/DatabaseTest.php @@ -0,0 +1,155 @@ +adminConnection = $this->prophesize(AdminConnectionInterface::class); + $this->instance = $this->prophesize(Instance::class); + $this->instance->name()->willReturn(self::INSTANCE_NAME); + + $this->database = new DatabaseStub( + $this->adminConnection->reveal(), + $this->instance->reveal(), + self::PROJECT_ID, + self::NAME + ); + } + + public function testName() + { + $this->assertEquals(self::NAME, $this->database->name()); + } + + public function testExists() + { + $this->adminConnection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willReturn([]); + + $this->database->setAdminConnection($this->adminConnection->reveal()); + + $this->assertTrue($this->database->exists()); + } + + public function testExistsNotFound() + { + $this->adminConnection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willThrow(new NotFoundException('', 404)); + + $this->database->setAdminConnection($this->adminConnection->reveal()); + + $this->assertFalse($this->database->exists()); + } + + public function testUpdate() + { + $statements = ['foo', 'bar']; + $this->adminConnection->updateDatabase([ + 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), + 'statements' => $statements + ]); + + $this->database->setAdminConnection($this->adminConnection->reveal()); + + $this->database->update($statements); + } + + public function testUpdateWithSingleStatement() + { + $statement = 'foo'; + $this->adminConnection->updateDatabase([ + 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), + 'statements' => ['foo'], + 'operationId' => null, + ])->shouldBeCalled(); + + $this->database->setAdminConnection($this->adminConnection->reveal()); + + $this->database->update($statement); + } + + public function testDrop() + { + $this->adminConnection->dropDatabase([ + 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) + ])->shouldBeCalled(); + + $this->database->setAdminConnection($this->adminConnection->reveal()); + + $this->database->drop(); + } + + public function testDdl() + { + $ddl = ['create table users', 'create table posts']; + $this->adminConnection->getDatabaseDDL([ + 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) + ])->willReturn(['statements' => $ddl]); + + $this->database->setAdminConnection($this->adminConnection->reveal()); + + $this->assertEquals($ddl, $this->database->ddl()); + } + + public function testDdlNoResult() + { + $this->adminConnection->getDatabaseDDL([ + 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) + ])->willReturn([]); + + $this->database->setAdminConnection($this->adminConnection->reveal()); + + $this->assertEquals([], $this->database->ddl()); + } + + public function testIam() + { + $this->assertInstanceOf(Iam::class, $this->database->iam()); + } +} + +class DatabaseStub extends Database +{ + public function setAdminConnection($conn) + { + $this->adminConnection = $conn; + } +} diff --git a/tests/Spanner/InstanceTest.php b/tests/Spanner/InstanceTest.php new file mode 100644 index 000000000000..62bef6343878 --- /dev/null +++ b/tests/Spanner/InstanceTest.php @@ -0,0 +1,318 @@ +adminConnection = $this->prophesize(AdminConnectionInterface::class); + $this->instance = new InstanceStub($this->adminConnection->reveal(), self::PROJECT_ID, self::NAME); + } + + public function testName() + { + $this->assertEquals(self::NAME, $this->instance->name()); + } + + public function testInfo() + { + $this->adminConnection->getInstance()->shouldNotBeCalled(); + + $instance = new Instance($this->adminConnection->reveal(), self::PROJECT_ID, self::NAME, ['foo' => 'bar']); + $this->assertEquals('bar', $instance->info()['foo']); + } + + public function testInfoWithReload() + { + $instance = $this->getDefaultInstance(); + + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $info = $this->instance->info(); + $this->assertEquals('Instance Name', $info['displayName']); + + $this->assertEquals($info, $this->instance->info()); + } + + public function testExists() + { + $this->adminConnection->getInstance(Argument::any())->shouldBeCalled()->willReturn([]); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->assertTrue($this->instance->exists()); + } + + public function testExistsNotFound() + { + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalled() + ->willThrow(new NotFoundException('foo', 404)); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->assertFalse($this->instance->exists()); + } + + public function testReload() + { + $instance = $this->getDefaultInstance(); + + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $info = $this->instance->reload(); + + $this->assertEquals('Instance Name', $info['displayName']); + } + + public function testState() + { + $instance = $this->getDefaultInstance(); + + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->assertEquals(Instance::STATE_READY, $this->instance->state()); + } + + public function testStateIsNull() + { + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn([]); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->assertNull($this->instance->state()); + } + + public function testUpdate() + { + $instance = $this->getDefaultInstance(); + + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->adminConnection->updateInstance([ + 'name' => $instance['name'], + 'displayName' => $instance['displayName'], + 'nodeCount' => $instance['nodeCount'], + 'labels' => [], + 'config' => $instance['config'] + ])->shouldBeCalled(); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->instance->update(); + } + + public function testUpdateWithExistingLabels() + { + $instance = $this->getDefaultInstance(); + $instance['labels'] = ['foo' => 'bar']; + + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->adminConnection->updateInstance([ + 'name' => $instance['name'], + 'displayName' => $instance['displayName'], + 'nodeCount' => $instance['nodeCount'], + 'labels' => $instance['labels'], + 'config' => $instance['config'] + ])->shouldBeCalled(); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->instance->update(); + } + + public function testUpdateWithChanges() + { + $instance = $this->getDefaultInstance(); + + $config = $this->prophesize(Configuration::class); + $config->name()->willReturn('config-name'); + + $changes = [ + 'labels' => [ + 'foo' => 'bar' + ], + 'nodeCount' => 900, + 'displayName' => 'New Name', + 'config' => $config->reveal() + ]; + + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->adminConnection->updateInstance([ + 'name' => $instance['name'], + 'displayName' => $changes['displayName'], + 'nodeCount' => $changes['nodeCount'], + 'labels' => $changes['labels'], + 'config' => InstanceAdminApi::formatInstanceConfigName(self::PROJECT_ID, $changes['config']->name()) + ])->shouldBeCalled(); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->instance->update($changes); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testUpdateInvalidConfig() + { + $instance = $this->getDefaultInstance(); + + $changes = [ + 'config' => 'foo' + ]; + + $this->adminConnection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn($instance); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->instance->update($changes); + } + + public function testDelete() + { + $this->adminConnection->deleteInstance([ + 'name' => InstanceAdminApi::formatInstanceName(self::PROJECT_ID, self::NAME) + ])->shouldBeCalled(); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $this->instance->delete(); + } + + public function testCreateDatabase() + { + $dbInfo = [ + 'name' => 'test-database' + ]; + + $extra = ['foo', 'bar']; + + $this->adminConnection->createDatabase([ + 'instance' => InstanceAdminApi::formatInstanceName(self::PROJECT_ID, self::NAME), + 'createStatement' => 'CREATE DATABASE `test-database`', + 'extraStatements' => $extra + ]) + ->shouldBeCalled() + ->willReturn($dbInfo); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $database = $this->instance->createDatabase('test-database', [ + 'statements' => $extra + ]); + + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals('test-database', $database->name()); + } + + public function testDatabase() + { + $database = $this->instance->database('test-database'); + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals('test-database', $database->name()); + } + + public function testDatabases() + { + $databases = [ + ['name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')], + ['name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] + ]; + + $this->adminConnection->listDatabases(Argument::any()) + ->shouldBeCalled() + ->willReturn(['databases' => $databases]); + + $this->instance->setAdminConnection($this->adminConnection->reveal()); + + $dbs = $this->instance->databases(); + + $this->assertInstanceOf(\Generator::class, $dbs); + + $dbs = iterator_to_array($dbs); + + $this->assertEquals(2, count($dbs)); + $this->assertEquals('database1', $dbs[0]->name()); + $this->assertEquals('database2', $dbs[1]->name()); + } + + public function testIam() + { + $this->assertInstanceOf(Iam::class, $this->instance->iam()); + } + + // ************** // + + private function getDefaultInstance() + { + return json_decode(file_get_contents(__DIR__ .'/../fixtures/spanner/instance.json'), true); + } +} + +class InstanceStub extends Instance +{ + public function setAdminConnection($conn) + { + $this->adminConnection = $conn; + } +} diff --git a/tests/Spanner/SpannerClientTest.php b/tests/Spanner/SpannerClientTest.php new file mode 100644 index 000000000000..0ec7198d7862 --- /dev/null +++ b/tests/Spanner/SpannerClientTest.php @@ -0,0 +1,154 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); + + $this->client = new SpannerClientStub(['projectId' => 'test-project']); + $this->client->setConnection($this->connection->reveal()); + $this->client->setAdminConnection($this->adminConnection->reveal()); + } + + public function testConfigurations() + { + $this->adminConnection->listConfigs(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instanceConfigs' => [ + [ + 'name' => 'projects/foo/instanceConfigs/bar', + 'displayName' => 'Bar' + ], [ + 'name' => 'projects/foo/instanceConfigs/bat', + 'displayName' => 'Bat' + ] + ] + ]); + + $this->client->setAdminConnection($this->adminConnection->reveal()); + + $configs = $this->client->configurations(); + + $this->assertInstanceOf(\Generator::class, $configs); + + $configs = iterator_to_array($configs); + $this->assertEquals(2, count($configs)); + $this->assertInstanceOf(Configuration::class, $configs[0]); + $this->assertInstanceOf(Configuration::class, $configs[1]); + } + + public function testConfiguration() + { + $config = $this->client->configuration('bar'); + + $this->assertInstanceOf(Configuration::class, $config); + $this->assertEquals('bar', $config->name()); + } + + public function testCreateInstance() + { + $this->adminConnection->createInstance(Argument::that(function ($arg) { + if ($arg['name'] !== 'projects/test-project/instances/foo') return false; + if ($arg['config'] !== 'projects/test-project/instanceConfigs/my-config') return false; + + return true; + })) + ->shouldBeCalled() + ->willReturn([]); + + $this->client->setAdminConnection($this->adminConnection->reveal()); + + $config = $this->prophesize(Configuration::class); + $config->name()->willReturn('my-config'); + + $i = $this->client->createInstance($config->reveal(), 'foo'); + + $this->assertInstanceOf(Instance::class, $i); + $this->assertEquals('foo', $i->name()); + } + + public function testInstance() + { + $i = $this->client->instance('foo'); + $this->assertInstanceOf(Instance::class, $i); + $this->assertEquals('foo', $i->name()); + } + + public function testInstanceWithInstanceArray() + { + $i = $this->client->instance('foo', ['key' => 'val']); + $this->assertEquals('val', $i->info()['key']); + } + + public function testInstances() + { + $this->adminConnection->listInstances(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instances' => [ + ['name' => 'projects/test-project/instances/foo'], + ['name' => 'projects/test-project/instances/bar'], + ] + ]); + + $this->client->setAdminConnection($this->adminConnection->reveal()); + + $instances = $this->client->instances(); + $this->assertInstanceOf(\Generator::class, $instances); + + $instances = iterator_to_array($instances); + $this->assertEquals(2, count($instances)); + $this->assertEquals('foo', $instances[0]->name()); + $this->assertEquals('bar', $instances[1]->name()); + } +} + +class SpannerClientStub extends SpannerClient +{ + public function setConnection($conn) + { + $this->connection = $conn; + } + + public function setAdminConnection($conn) + { + $this->adminConnection = $conn; + } +} diff --git a/tests/fixtures/spanner/instance.json b/tests/fixtures/spanner/instance.json new file mode 100644 index 000000000000..fcf371769ce3 --- /dev/null +++ b/tests/fixtures/spanner/instance.json @@ -0,0 +1,7 @@ +{ + "name": "projects\/test-project\/instances\/instance-name", + "config": "projects\/test-project\/instanceConfigs\/regional-europe-west1", + "displayName": "Instance Name", + "nodeCount": 1, + "state": 2 +} diff --git a/tests/unit/Datastore/OperationTest.php b/tests/unit/Datastore/OperationTest.php index 7f9603180721..62c6f379bce9 100644 --- a/tests/unit/Datastore/OperationTest.php +++ b/tests/unit/Datastore/OperationTest.php @@ -891,6 +891,11 @@ public function testInvalidBatchType() class OperationStub extends Operation { + // public function runQuery(QueryInterface $q, array $args = []) + // { + // echo 'test'; + // exit; + // } public function setConnection($connection) { $this->connection = $connection; From c6124c1ea764bfcbbc925be8672fb37781debcb8 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 26 Oct 2016 15:47:45 -0400 Subject: [PATCH 003/107] Add Spanner Client --- src/Spanner/Configuration.php | 175 ++++++ .../Connection/ConnectionInterface.php | 151 +++++ src/Spanner/Connection/Grpc.php | 485 ++++++++++++++++ src/Spanner/Connection/IamDatabase.php | 54 ++ src/Spanner/Connection/IamInstance.php | 54 ++ src/Spanner/Database.php | 548 ++++++++++++++++++ src/Spanner/Instance.php | 407 +++++++++++++ src/Spanner/KeyRange.php | 91 +++ src/Spanner/KeySet.php | 101 ++++ src/Spanner/Operation.php | 231 ++++++++ src/Spanner/Result.php | 166 ++++++ src/Spanner/Session/Session.php | 159 +++++ src/Spanner/Session/SessionClient.php | 104 ++++ src/Spanner/Session/SessionPool.php | 48 ++ src/Spanner/Session/SessionPoolInterface.php | 26 + src/Spanner/Session/SimpleSessionPool.php | 45 ++ src/Spanner/SpannerClient.php | 302 ++++++++++ src/Spanner/Transaction.php | 249 ++++++++ 18 files changed, 3396 insertions(+) create mode 100644 src/Spanner/Configuration.php create mode 100644 src/Spanner/Connection/ConnectionInterface.php create mode 100644 src/Spanner/Connection/Grpc.php create mode 100644 src/Spanner/Connection/IamDatabase.php create mode 100644 src/Spanner/Connection/IamInstance.php create mode 100644 src/Spanner/Database.php create mode 100644 src/Spanner/Instance.php create mode 100644 src/Spanner/KeyRange.php create mode 100644 src/Spanner/KeySet.php create mode 100644 src/Spanner/Operation.php create mode 100644 src/Spanner/Result.php create mode 100644 src/Spanner/Session/Session.php create mode 100644 src/Spanner/Session/SessionClient.php create mode 100644 src/Spanner/Session/SessionPool.php create mode 100644 src/Spanner/Session/SessionPoolInterface.php create mode 100644 src/Spanner/Session/SimpleSessionPool.php create mode 100644 src/Spanner/SpannerClient.php create mode 100644 src/Spanner/Transaction.php diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php new file mode 100644 index 000000000000..201832c8a526 --- /dev/null +++ b/src/Spanner/Configuration.php @@ -0,0 +1,175 @@ +spanner(); + * + * $configuration = $spanner->configuration('regional-europe-west'); + * ``` + */ +class Configuration +{ + /** + * @var ConnectionInterface + */ + private $connection; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var array + */ + private $info; + + /** + * Create a configuration instance. + * + * @param ConnectionInterface $connection A service connection for the + * Spanner API. + * @param string $projectId The current project ID. + * @param string $name The simple configuration name. + * @param array $info [optional] A service representation of the + * configuration. + */ + public function __construct( + ConnectionInterface $connection, + $projectId, + $name, + array $info = [] + ) { + $this->connection = $connection; + $this->projectId = $projectId; + $this->name = $name; + $this->info = $info; + } + + /** + * Return the configuration name. + * + * Example: + * ``` + * $name = $configuration->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Return the service representation of the configuration. + * + * This method may require a service call. + * + * Example: + * ``` + * $info = $configuration->info(); + * echo $info['nodeCount']; + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function info(array $options = []) + { + if (!$this->info) { + $this->reload($options); + } + + return $this->info; + } + + /** + * Check if the configuration exists. + * + * This method requires a service call. + * + * Example: + * ``` + * if ($configuration->exists()) { + * echo 'The configuration exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function exists(array $options = []) + { + try { + $this->reload($options = []); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Fetch a fresh representation of the configuration from the service. + * + * Example: + * ``` + * $info = $configuration->reload(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function reload(array $options = []) + { + $this->info = $this->connection->getConfig($options + [ + 'name' => InstanceAdminApi::formatInstanceConfigName($this->projectId, $this->name), + 'projectId' => $this->projectId + ]); + + return $this->info; + } + + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'name' => $this->name, + 'info' => $this->info, + ]; + } +} diff --git a/src/Spanner/Connection/ConnectionInterface.php b/src/Spanner/Connection/ConnectionInterface.php new file mode 100644 index 000000000000..e041a94c9f10 --- /dev/null +++ b/src/Spanner/Connection/ConnectionInterface.php @@ -0,0 +1,151 @@ + 'setInsert', + 'update' => 'setUpdate', + 'upsert' => 'setInsertOrUpdate', + 'replace' => 'replace', + ]; + + /** + * @param array $config + */ + public function __construct(array $config) + { + $grpcConfig = [ + 'credentialsLoader' => CredentialsLoader::makeCredentials($config['scopes'], $config['keyFile']) + ]; + + $this->codec = new PhpArray([ + 'timestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + } + ]); + + $config['codec'] = $this->codec; + $this->setRequestWrapper(new GrpcRequestWrapper($config)); + + $this->instanceAdminApi = new InstanceAdminApi($grpcConfig); + $this->databaseAdminApi = new DatabaseAdminApi($grpcConfig); + $this->spannerApi = new SpannerApi($grpcConfig); + } + + /** + * @param array $args [optional] + */ + public function listConfigs(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'listInstanceConfigs'], [ + $args['projectId'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getConfig(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'getInstanceConfig'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function listInstances(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'listInstances'], [ + InstanceAdminApi::formatProjectName($args['projectId']), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getInstance(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'getInstance'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function createInstance(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'createInstance'], [ + $args['name'], + $args['config'], + $args['displayName'], + $args['nodeCount'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function updateInstance(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'updateInstance'], [ + $args['name'], + $args['config'], + $args['displayName'], + $args['nodeCount'], + new State, + $args['labels'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function deleteInstance(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'deleteInstance'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getInstanceIamPolicy(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'getIamPolicy'], [ + $args['resource'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function setInstanceIamPolicy(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'setIamPolicy'], [ + $args['resource'], + $args['policy'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function testInstanceIamPermissions(array $args = []) + { + return $this->send([$this->instanceAdminApi, 'testIamPermissions'], [ + $args['resource'], + $args['permissions'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function listDatabases(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'listDatabases'], [ + $args['instance'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function createDatabase(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'createDatabase'], [ + $args['instance'], + $args['createStatement'], + $args['extraStatements'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function updateDatabase(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'updateDatabase'], [ + $args['name'], + $args['statements'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function dropDatabase(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'dropDatabase'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getDatabaseDDL(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'getDatabaseDDL'], [ + $args['name'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getDatabaseIamPolicy(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'getIamPolicy'], [ + $args['resource'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function setDatabaseIamPolicy(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'setIamPolicy'], [ + $args['resource'], + $args['policy'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function testDatabaseIamPermissions(array $args = []) + { + return $this->send([$this->databaseAdminApi, 'testIamPermissions'], [ + $args['resource'], + $args['permissions'], + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function createSession(array $args = []) + { + return $this->send([$this->spannerApi, 'createSession'], [ + $this->pluck('database', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function getSession(array $args = []) + { + return $this->send([$this->spannerApi, 'getSession'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function deleteSession(array $args = []) + { + return $this->send([$this->spannerApi, 'deleteSession'], [ + $this->pluck('name', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function executeSql(array $args = []) + { + $args['params'] = (new protobuf\Struct) + ->deserialize($this->formatStructForApi($args['params']), $this->codec); + + return $this->send([$this->spannerApi, 'executeSql'], [ + $this->pluck('session', $args), + $this->pluck('sql', $args), + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function read(array $args = []) + { + $keys = $this->pluck('keySet', $args); + + $keySet = new v1\KeySet; + if (!empty($keys['keys'])) { + $keySet->setKeys($this->formatListForApi($keys['keys'])); + } + + if (!empty($keys['ranges'])) { + $ranges = new v1\KeyRange; + + if (isset($keys['ranges']['startClosed'])) { + $ranges->setStartClosed($this->formatListForApi($keys['ranges']['startClosed'])); + } + + if (isset($keys['ranges']['startOpen'])) { + $ranges->setStartOpen($this->formatListForApi($keys['ranges']['startOpen'])); + } + if (isset($keys['ranges']['endClosed'])) { + $ranges->setEndClosed($this->formatListForApi($keys['ranges']['endClosed'])); + } + if (isset($keys['ranges']['endOpen'])) { + $ranges->setEndOpen($this->formatListForApi($keys['ranges']['endOpen'])); + } + + $keySet->setRanges($ranges); + } + + if (isset($keys['all'])) { + $keySet->setAll($keys['all']); + } + + return $this->send([$this->spannerApi, 'read'], [ + $this->pluck('session', $args), + $this->pluck('table', $args), + $this->pluck('columns', $args), + $keySet, + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function beginTransaction(array $args = []) + { + $options = new TransactionOptions; + + if (isset($args['readOnly'])) { + $readOnly = (new TransactionOptions\ReadOnly) + ->deserialize($args['readOnly'], $this->codec); + + $options->setReadOnly($readOnly); + } else { + $readWrite = new TransactionOptions\ReadWrite(); + $options->setReadWrite($readWrite); + } + + return $this->send([$this->spannerApi, 'beginTransaction'], [ + $this->pluck('session', $args), + $options, + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function commit(array $args = []) + { + $inputMutations = $this->pluck('mutations', $args); + + $mutations = []; + if (is_array($inputMutations)) { + foreach ($inputMutations as $mutation) { + $type = array_keys($mutation)[0]; + $data = $mutation[$type]; + $data['values'] = $this->formatListForApi($data['values']); + + switch ($type) { + case 'insert': + case 'update': + case 'upsert': + case 'replace': + $write = (new Mutation\Write) + ->deserialize($data, $this->codec); + + $setterName = $this->mutationSetters[$type]; + $mutation = new Mutation; + $mutation->$setterName($write); + $mutations[] = $mutation; + + break; + + case 'delete': + $mutations[] = (new Mutation\Delete) + ->deserialize($data, $this->codec); + + break; + } + } + } + + if (isset($args['singleUseTransaction'])) { + $options = new TransactionOptions; + $readWrite = (new TransactionOptions\ReadWrite) + ->deserialize($args['singleUseTransaction']['readWrite'], $this->codec); + $options->setReadWrite($readWrite); + $args['singleUseTransaction'] = $options; + } + + return $this->send([$this->spannerApi, 'commit'], [ + $this->pluck('session', $args), + $mutations, + $args + ]); + } + + /** + * @param array $args [optional] + */ + public function rollback(array $args = []) + { + return $this->send([$this->spannerApi, 'rollback'], [ + $this->pluck('session', $args), + $this->pluck('transactionId', $args), + $args + ]); + } +} diff --git a/src/Spanner/Connection/IamDatabase.php b/src/Spanner/Connection/IamDatabase.php new file mode 100644 index 000000000000..9461cb304d7e --- /dev/null +++ b/src/Spanner/Connection/IamDatabase.php @@ -0,0 +1,54 @@ +adminConnection = $adminConnection; + } + + /** + * @param array $args + */ + public function getPolicy(array $args) + { + return $this->adminConnection->getDatabaseIamPolicy($args); + } + + /** + * @param array $args + */ + public function setPolicy(array $args) + { + return $this->adminConnection->setDatabaseIamPolicy($args); + } + + /** + * @param array $args + */ + public function testPermissions(array $args) + { + return $this->adminConnection->testDatabaseIamPermissions($args); + } +} diff --git a/src/Spanner/Connection/IamInstance.php b/src/Spanner/Connection/IamInstance.php new file mode 100644 index 000000000000..794c83c29d9a --- /dev/null +++ b/src/Spanner/Connection/IamInstance.php @@ -0,0 +1,54 @@ +adminConnection = $adminConnection; + } + + /** + * @param array $args + */ + public function getPolicy(array $args) + { + return $this->adminConnection->getInstanceIamPolicy($args); + } + + /** + * @param array $args + */ + public function setPolicy(array $args) + { + return $this->adminConnection->setInstanceIamPolicy($args); + } + + /** + * @param array $args + */ + public function testPermissions(array $args) + { + return $this->adminConnection->testInstanceIamPermissions($args); + } +} diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php new file mode 100644 index 000000000000..73c0eaaf786e --- /dev/null +++ b/src/Spanner/Database.php @@ -0,0 +1,548 @@ +database('my-database'); + * ``` + */ +class Database +{ + /** + * @var ConnectionInterface + */ + private $connection; + + /** + * @var Instance + */ + private $instance; + + /** + * @var SessionPoolInterface + */ + private $sessionPool; + + /** + * @var Operation + */ + private $operation; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var Iam + */ + private $iam; + + /** + * Create an object representing a Database. + * + * @param ConnectionInterface $connection The connection to the + * Google Cloud Spanner Admin API. + * @param Instance $instance The instance in which the database exists. + * @param SessionPoolInterface The session pool implementation. + * @param string $projectId The project ID. + * @param string $name The database name. + * @param array $info [optional] A representation of the database object. + */ + public function __construct( + ConnectionInterface $connection, + Instance $instance, + SessionPoolInterface $sessionPool, + $projectId, + $name + ) { + $this->connection = $connection; + $this->instance = $instance; + $this->sessionPool = $sessionPool; + $this->projectId = $projectId; + $this->name = $name; + + $this->operation = new Operation($connection, $instance, $this); + $this->iam = new Iam( + new IamDatabase($this->connection), + $this->fullyQualifiedDatabaseName() + ); + } + + /** + * Return the simple database name. + * + * Example: + * ``` + * $name = $database->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Check if the database exists. + * + * This method sends a service request. + * + * Example: + * ``` + * if ($database->exists()) { + * echo 'The database exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return bool + */ + public function exists(array $options = []) + { + try { + $this->connection->getDatabaseDDL($options + [ + 'name' => $this->fullyQualifiedDatabaseName() + ]); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Update the Database. + * + * Example: + * ``` + * $database->update([ + * 'CREATE TABLE Users ( + * id INT64 NOT NULL, + * name STRING(100) NOT NULL + * password STRING(100) NOT NULL + * )' + * ]); + * ``` + * + * @param string|array $statements One or more DDL statements to execute. + * @param array $options [optional] Configuration options. + * @return + */ + public function updateDdl($statements, array $options = []) + { + $options += [ + 'operationId' => null + ]; + + if (!is_array($statements)) { + $statements = [$statements]; + } + + return $this->connection->updateDatabase($options + [ + 'name' => $this->fullyQualifiedDatabaseName(), + 'statements' => $statements, + ]); + } + + /** + * Drop the database. + * + * Example: + * ``` + * $database->drop(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return void + */ + public function drop(array $options = []) + { + return $this->connection->dropDatabase($options + [ + 'name' => $this->fullyQualifiedDatabaseName() + ]); + } + + /** + * Get a list of all database DDL statements. + * + * Example: + * ``` + * $statements = $database->ddl(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function ddl(array $options = []) + { + $ddl = $this->connection->getDatabaseDDL($options + [ + 'name' => $this->fullyQualifiedDatabaseName() + ]); + + if (isset($ddl['statements'])) { + return $ddl['statements']; + } + + return []; + } + + /** + * Manage the database IAM policy + * + * Example: + * ``` + * $iam = $database->iam(); + * ``` + * + * @return Iam + */ + public function iam() + { + return $this->iam; + } + + /** + * Create a Read Only transaction + * + * @codingStandardsIgnoreStart + * @param array $options [optional] { + * Configuration Options + * + * @type array $transactionOptions [TransactionOptions](https://cloud.google.com/spanner/reference/rest/v1/TransactionOptions). + * } + * @codingStandardsIgnoreEnd + * @return Transaction + */ + public function readOnlyTransaction(array $options = []) + { + $options += [ + 'transactionOptions' => [] + ]; + + if (empty($options['transactionOptions'])) { + $options['transactionOptions']['strong'] = true; + } + + $options['readOnly'] = $options['transactionOptions']; + + return $this->transaction(SessionPoolInterface::CONTEXT_READ, $options); + } + + /** + * Create a Read/Write transaction + * + * @param array $options [optional] Configuration Options + * @return Transaction + */ + public function lockingTransaction(array $options = []) + { + $options['readWrite'] = []; + + return $this->transaction(SessionPoolInterface::CONTEXT_READWRITE, $options); + } + + /** + * Insert a row. + * + * @param string $table The table to mutate. + * @param array $data The row data to insert. + * @param array $options [optional] Configuration options. + * @return array + */ + public function insert($table, array $data, array $options = []) + { + return $this->insertBatch($table, [$data], $options); + } + + /** + * Insert multiple rows. + * + * @param string $table The table to mutate. + * @param array $dataSet The row data to insert. + * @param array $options [optional] Configuration options. + * @return array + */ + public function insertBatch($table, array $dataSet, array $options = []) + { + $mutations = []; + foreach ($dataSet as $data) { + $mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Update a row. + * + * @param string $table The table to mutate. + * @param array $data The row data to update. + * @param array $options [optional] Configuration options. + * @return array + */ + public function update($table, array $data, array $options = []) + { + return $this->updateBatch($table, [$data], $options); + } + + /** + * Update multiple rows. + * + * @param string $table The table to mutate. + * @param array $dataSet The row data to update. + * @param array $options [optional] Configuration options. + * @return array + */ + public function updateBatch($table, array $dataSet, array $options = []) + { + $mutations = []; + foreach ($dataSet as $data) { + $mutations[] = $this->operation->mutation(Operation::OP_UPDATE, $table, $data); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Insert or update a row. + * + * @param string $table The table to mutate. + * @param array $data The row data to insert or update. + * @param array $options [optional] Configuration options. + * @return array + */ + public function insertOrUpdate($table, array $data, array $options = []) + { + return $this->insertOrUpdateBatch($table, [$data], $options); + } + + /** + * Insert or update multiple rows. + * + * @param string $table The table to mutate. + * @param array $dataSet The row data to insert or update. + * @param array $options [optional] Configuration options. + * @return array + */ + public function insertOrUpdateBatch($table, array $dataSet, array $options = []) + { + $mutations = []; + foreach ($dataSet as $data) { + $mutations[] = $this->operation->mutation(Operation::OP_INSERT_OR_UPDATE, $table, $data); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Replace a row. + * + * @param string $table The table to mutate. + * @param array $data The row data to replace. + * @param array $options [optional] Configuration options. + * @return array + */ + public function replace($table, array $data, array $options = []) + { + return $this->replaceBatch($table, [$data], $options); + } + + /** + * Replace multiple rows. + * + * @param string $table The table to mutate. + * @param array $dataSet The row data to replace. + * @param array $options [optional] Configuration options. + * @return array + */ + public function replaceBatch($table, array $dataSet, array $options = []) + { + $mutations = []; + foreach ($dataSet as $data) { + $mutations[] = $this->operation->mutation(Operation::OP_REPLACE, $table, $data); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Delete a row. + * + * @param string $table The table to mutate. + * @param array $key The key to use to identify the row or rows to delete. + * @param array $options [optional] Configuration options. + * @return array + */ + public function delete($table, array $key, array $options = []) + { + return $this->deleteBatch($table, [$key], $options); + } + + /** + * Delete multiple rows. + * + * @param string $table The table to mutate. + * @param array $keySets The keys to use to identify the row or rows to delete. + * @param array $options [optional] Configuration options. + * @return array + */ + public function deleteBatch($table, array $keySets, array $options = []) + { + $mutations = []; + foreach ($keySets as $keySet) { + $mutations[] = $this->operation->deleteMutation($table, $keySet); + } + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + return $this->operation->commit($session, $mutations, $options); + } + + /** + * Run a query. + * + * @param string $sql The query string to execute. + * @param array $options [optional] Configuration options. + * @return Result + */ + public function execute($sql, array $options = []) + { + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + + return $this->operation->execute($session, $sql, $options); + } + + /** + * Lookup rows in a table. + * + * Note that if no KeySet is specified, all rows in a table will be + * returned. + * + * @todo is returning everything a reasonable default? + * + * @param string $table The table name. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $index The name of an index on the table. + * @type array $columns A list of column names to be returned. + * @type array $keySet A [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * } + */ + public function read($table, array $options = []) + { + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + + return $this->operation->read($session, $table, $options); + } + + /** + * Create a transaction with a given context. + * + * @param string $context The context of the new transaction. + * @param array $options [optional] Configuration options. + * @return Transaction + */ + private function transaction($context, array $options = []) + { + $options += [ + 'transactionOptions' => [] + ]; + + $session = $this->selectSession($context); + + // make a service call here. + $res = $this->connection->beginTransaction($options + [ + 'session' => $session->name(), + 'context' => $context, + ]); + + return new Transaction($this->operation, $session, $context, $res); + } + + /** + * Retrieve a session from the session pool. + * + * @param string $context The session context. + * @return Session + */ + private function selectSession($context = SessionPoolInterface::CONTEXT_READ) { + return $this->sessionPool->session( + $this->instance->name(), + $this->name, + $context + ); + } + + /** + * Convert the simple database name to a fully qualified name. + * + * @return string + */ + private function fullyQualifiedDatabaseName() + { + return DatabaseAdminApi::formatDatabaseName( + $this->projectId, + $this->instance->name(), + $this->name + ); + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'name' => $this->name + ]; + } +} diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php new file mode 100644 index 000000000000..d03ec9aa6332 --- /dev/null +++ b/src/Spanner/Instance.php @@ -0,0 +1,407 @@ +instance('my-instance'); + * ``` + */ +class Instance +{ + const STATE_READY = State::READY; + const STATE_CREATING = State::CREATING; + + /** + * @var ConnectionInterface + */ + private $connection; + + /** + * @var SessionPool; + */ + private $sessionPool; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $name; + + /** + * @var array + */ + private $info; + + /** + * @var Iam + */ + private $iam; + + /** + * Create an object representing a Google Cloud Spanner instance. + * + * @param ConnectionInterface $connection The connection to the + * Google Cloud Spanner Admin API. + * @param SessionPoolInterface $sessionPool The session pool implementation. + * @param string $projectId The project ID. + * @param string $name The instance name. + * @param array $info [optional] A representation of the instance object. + */ + public function __construct( + ConnectionInterface $connection, + SessionPoolInterface $sessionPool, + $projectId, + $name, + array $info = [] + ) { + $this->connection = $connection; + $this->sessionPool = $sessionPool; + $this->projectId = $projectId; + $this->name = $name; + $this->info = $info; + $this->iam = new Iam( + new IamInstance($this->connection), + $this->fullyQualifiedInstanceName() + ); + } + + /** + * Return the instance name. + * + * Example: + * ``` + * $name = $instance->name(); + * ``` + * + * @return string + */ + public function name() + { + return $this->name; + } + + /** + * Return the service representation of the instance. + * + * This method may require a service call. + * + * Example: + * ``` + * $info = $instance->info(); + * echo $info['nodeCount']; + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function info(array $options = []) + { + if (!$this->info) { + $this->reload($options); + } + + return $this->info; + } + + /** + * Check if the instance exists. + * + * This method requires a service call. + * + * Example: + * ``` + * if ($instance->exists()) { + * echo 'The instance exists!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function exists(array $options = []) + { + try { + $this->reload($options = []); + } catch (NotFoundException $e) { + return false; + } + + return true; + } + + /** + * Fetch a fresh representation of the instance from the service. + * + * Example: + * ``` + * $info = $instance->reload(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function reload(array $options = []) + { + $this->info = $this->connection->getInstance($options + [ + 'name' => $this->fullyQualifiedInstanceName() + ]); + + return $this->info; + } + + /** + * Return the instance state. + * + * When instances are created or updated, they may take some time before + * they are ready for use. This method allows for checking whether an + * instance is ready. + * + * Example: + * ``` + * $instance = $spanner->createInstance($config, 'my-new-instance'); + * if ($instance->state() === Instance::STATE_READY) { + * // do stuff + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return string + */ + public function state(array $options = []) + { + $info = $this->info($options); + + return (isset($info['state'])) + ? $info['state'] + : null; + } + + /** + * Update the instance + * + * Example: + * ``` + * todo + * ``` + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1 Update Instance + * + * @param array $options { + * Configuration options + * + * @type Configuration $config The configuration to move the instante to. + * @return void + * @throws \InvalidArgumentException + */ + public function update(array $options = []) + { + $info = $this->info($options); + + $options += [ + 'displayName' => $info['displayName'], + 'nodeCount' => $info['nodeCount'], + 'config' => null, + 'labels' => (isset($info['labels'])) + ? $info['labels'] + : [] + ]; + + $config = $info['config']; + if ($options['config']) { + if (!($options['config'] instanceof Configuration)) { + throw new \InvalidArgumentException( + 'Given configuration is not an instance of Configuration.' + ); + } + + $config = InstanceAdminApi::formatInstanceConfigName( + $this->projectId, + $options['config']->name() + ); + } + + $this->connection->updateInstance([ + 'name' => $this->fullyQualifiedInstanceName(), + 'config' => $config, + ] + $options); + } + + /** + * Delete the instance, any databases in the instance, and all data. + * + * Example: + * ``` + * $instance->delete(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return void + */ + public function delete(array $options = []) + { + return $this->connection->deleteInstance($options + [ + 'name' => $this->fullyQualifiedInstanceName() + ]); + } + + /** + * Create a Database + * + * Example: + * ``` + * $database = $instance->createDatabase('my-database'); + * ``` + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/create Create Database + * + * @param string $name The database name. + * @param array $options [optional] { + * Configuration Options + * + * @type array $statements Additional DDL statements. + * } + * @return Database + */ + public function createDatabase($name, array $options = []) + { + $options += [ + 'statements' => [] + ]; + + $statement = sprintf('CREATE DATABASE `%s`', $name); + + $res = $this->connection->createDatabase([ + 'instance' => $this->fullyQualifiedInstanceName(), + 'createStatement' => $statement, + 'extraStatements' => $options['statements'] + ]); + + return $this->database($name); + } + + /** + * Lazily instantiate a database object + * + * Example: + * ``` + * $database = $instance->database('my-database'); + * ``` + * + * @param string $name The database name + * @return Database + */ + public function database($name) + { + return new Database( + $this->connection, + $this, + $this->sessionPool, + $this->projectId, + $name + ); + } + + /** + * List databases in an instance + * + * Example: + * ``` + * $databases = $instance->databases(); + * ``` + * + * @todo implement pagination! + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/list List Databases + * + * @param array $options Configuration options. + * @return \Generator + */ + public function databases(array $options = []) + { + $res = $this->connection->listDatabases($options + [ + 'instance' => $this->fullyQualifiedInstanceName(), + ]); + + $databases = []; + if (isset($res['databases'])) { + foreach ($res['databases'] as $database) { + yield $this->database( + DatabaseAdminApi::parseDatabaseFromDatabaseName($database['name']) + ); + } + } + } + + /** + * Manage the instance IAM policy + * + * Example: + * ``` + * $iam = $instance->iam(); + * ``` + * + * @return Iam + */ + public function iam() + { + return $this->iam; + } + + /** + * Convert the simple instance name to a fully qualified name. + * + * @return string + */ + private function fullyQualifiedInstanceName() + { + return InstanceAdminApi::formatInstanceName($this->projectId, $this->name); + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'name' => $this->name, + 'info' => $this->info + ]; + } +} diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php new file mode 100644 index 000000000000..729e8c43408f --- /dev/null +++ b/src/Spanner/KeyRange.php @@ -0,0 +1,91 @@ +startOpen = (isset($range['startOpen'])) + ? $range['startOpen'] + : null; + + $this->startClosed = (isset($range['startClosed'])) + ? $range['startClosed'] + : null; + + $this->endOpen = (isset($range['endOpen'])) + ? $range['endOpen'] + : null; + + $this->endClosed = (isset($range['endClosed'])) + ? $range['endClosed'] + : null; + + } + + public function setStartOpen($startOpen) + { + $this->startOpen = $startOpen; + } + + public function setStartClosed($startClosed) + { + $this->startClosed = $startClosed; + } + + public function setEndOpen($endOpen) + { + $this->endOpen = $endOpen; + } + + public function setEndClosed($endClosed) + { + $this->endClosed = $endClosed; + } + + public function keyRangeObject() + { + return [ + 'startOpen' => $this->startOpen, + 'startClosed' => $this->startClosed, + 'endOpen' => $this->endOpen, + 'endClosed' => $this->endClosed, + ]; + } +} diff --git a/src/Spanner/KeySet.php b/src/Spanner/KeySet.php new file mode 100644 index 000000000000..1de1af5c4f1e --- /dev/null +++ b/src/Spanner/KeySet.php @@ -0,0 +1,101 @@ + [], + 'ranges' => [], + 'all' => false + ]; + + $this->validateBatch($options['ranges'], KeyRange::class); + + $this->keys = $options['keys']; + $this->ranges = $options['ranges']; + $this->all = (bool) $options['all']; + } + + public function addRange(KeyRange $range) + { + $this->ranges[] = $range; + } + + public function setRanges(array $ranges) + { + $this->validateBatch($ranges, KeyRange::class); + + $this->ranges = $ranges; + } + + public function addKey($key) + { + $this->keys[] = $key; + } + + public function setKeys(array $keys) + { + $this->keys = $keys; + } + + public function setAll($all) + { + $this->all = (bool) $all; + } + + public function keySetObject() + { + $ranges = []; + foreach ($this->ranges as $range) { + $ranges[] = $range->keyRangeObject(); + } + + return [ + 'keys' => $this->keys, + 'ranges' => $ranges, + 'all' => $this->all + ]; + } +} diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php new file mode 100644 index 000000000000..0e05dd90f20a --- /dev/null +++ b/src/Spanner/Operation.php @@ -0,0 +1,231 @@ +connection = $connection; + $this->instance = $instance; + } + + /** + * Create a formatted mutation. + * + * @param string $operation The operation type. + * @param string $table The table name. + * @param array $mutation The mutation data, represented as a set of + * key/value pairs. + * @return array + */ + public function mutation($operation, $table, $mutation) + { + return [ + $operation => [ + 'table' => $table, + 'columns' => array_keys($mutation), + 'values' => array_values($mutation) + ] + ]; + } + + /** + * Create a formatted delete mutation. + * + * @param string $table The table name. + * @param array $keySet [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). + * @return array + */ + public function deleteMutation($table, $keySet) + { + return [ + 'delete' => [ + 'table' => $table, + 'keySet' => $keySet + ] + ]; + } + + /** + * Commit all enqueued mutations. + * + * @codingStandardsIgnoreStart + * @param Session $session The session ID to use for the commit. + * @param array $mutations The mutations to commit. + * @param array $options [optional] Configuration options. + * @return array [CommitResponse](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitResponse) + * @codingStandardsIgnoreEnd + */ + public function commit(Session $session, array $mutations, array $options = []) + { + if (!isset($options['transactionId'])) { + $options['singleUseTransaction'] = ['readWrite' => []]; + } + + try { + $res = $this->connection->commit([ + 'mutations' => $mutations, + 'session' => $session->name() + ] + $options); + + return $res; + } catch (\Exception $e) { + + // maybe do something here? + + throw $e; + } + } + + /** + * Rollback a Transaction + * + * @param Session $session The session to use for the rollback. + * Note that the session MUST be the same one in which the + * transaction was created. + * @param string $transactionId The transaction to roll back. + * @param array $options [optional] Configuration Options. + * @return void + */ + public function rollback(Session $session, $transactionId, array $options = []) + { + return $this->connection->rollback([ + 'transactionId' => $transactionId, + 'session' => $session->name() + ] + $options); + } + + /** + * Run a query + * + * @param Session $session The session to use to execute the SQL. + * @param string $sql The query string. + * @param array $options [optional] Configuration options. + * @return array + */ + public function execute(Session $session, $sql, array $options = []) + { + $options += [ + 'params' => [], + 'paramTypes' => [] + ]; + + $res = $this->connection->executeSql([ + 'sql' => $sql, + 'session' => $session->name() + ] + $options); + + return new Result($res); + } + + /** + * Lookup rows in a database. + * + * @param Session $session The session in which to read data. + * @param string $table The table to read from. + * @param array $options [optional] { + * Configuration Options + * + * @type string $index + * @type array $columns + * @type KeySet $keySet + * @type string $offset + * @type int $limit + * } + */ + public function read(Session $session, $table, array $options = []) + { + $options += [ + 'index' => null, + 'columns' => [], + 'keySet' => [], + 'offset' => null, + 'limit' => null, + ]; + + if (!empty($options['keySet']) && !($options['keySet']) instanceof KeySet) { + throw new RuntimeException('$options.keySet must be an instance of KeySet'); + } + + if (empty($options['keySet'])) { + $options['keySet'] = new KeySet(); + $options['keySet']->setAll(true); + } + + $options['keySet'] = $options['keySet']->keySetObject(); + + $res = $this->connection->read([ + 'table' => $table, + 'session' => $session->name() + ] + $options); + + return new Result($res); + } + + /** + * Represent the class in a more readable and digestable fashion. + * + * @access private + * @codeCoverageIgnore + */ + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'instance' => $this->instance, + 'sessionPool' => $this->sessionPool + ]; + } +} diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php new file mode 100644 index 000000000000..35cedd220956 --- /dev/null +++ b/src/Spanner/Result.php @@ -0,0 +1,166 @@ +result = $result; + $this->rows = $this->transformQueryResult($result); + } + + /** + * Return result metadata + * + * @return array [ResultSetMetadata](https://cloud.google.com/spanner/reference/rest/v1/ResultSetMetadata). + */ + public function metadata() + { + return $this->result['metadata']; + } + + /** + * Return the rows as represented by the API. + * + * For a more easily consumed result in which each row is represented as a + * set of key/value pairs, see {@see Google\Cloud\Spanner\Result::result()}. + * + * @return array|null + */ + public function rows() + { + return (isset($this->result['rows'])) + ? $result['rows'] + : null; + } + + /** + * Get the query plan and execution statistics for the query that produced + * this result set. + * + * Stats are not returned by default. + * + * @todo explain how to get dem stats. + * + * @return array|null [ResultSetStats](https://cloud.google.com/spanner/reference/rest/v1/ResultSetStats). + */ + public function stats() + { + return (isset($this->result['stats'])) + ? $result['stats'] + : null; + } + + /** + * Get the entire query or read response as given by the API. + * + * @return array [ResultSet](https://cloud.google.com/spanner/reference/rest/v1/ResultSet). + */ + public function info() + { + return $this->result; + } + + /** + * Transform the response from executeSql or read into a list of rows + * represented as a collection of key/value arrays. + * + * @param array $result + * @return array + */ + private function transformQueryResult(array $result) + { + if (!isset($result['rows']) || count($result['rows']) === 0) { + return null; + } + + $cols = []; + foreach (array_keys($result['rows'][0]) as $colIndex) { + $cols[] = $result['metadata']['rowType']['fields'][$colIndex]['name']; + } + + $rows = []; + foreach ($result['rows'] as $row) { + $rows[] = array_combine($cols, $row); + } + + return $rows; + } + + /** + * @access private + */ + public function rewind() + { + $this->index = 0; + } + + /** + * @access private + */ + public function current() + { + return $this->rows[$this->index]; + } + + /** + * @access private + */ + public function key() + { + return $this->index; + } + + /** + * @access private + */ + public function next() + { + ++$this->index; + } + + /** + * @access private + */ + public function valid() + { + return isset($this->rows[$this->index]); + } +} diff --git a/src/Spanner/Session/Session.php b/src/Spanner/Session/Session.php new file mode 100644 index 000000000000..62790689304b --- /dev/null +++ b/src/Spanner/Session/Session.php @@ -0,0 +1,159 @@ +spanner(); + * + * $sessionClient = $spanner->sessionClient(); + * $session = $sessionClient->create('test-instance', 'test-database'); + * ``` + */ +class Session +{ + /** + * @var ConnectionInterface + */ + private $connection; + + /** + * @var string + */ + private $projectId; + + /** + * @var string + */ + private $instance; + + /** + * @var string + */ + private $database; + + /** + * @var string + */ + private $name; + + /** + * @param ConnectionInterface $connection A connection to Cloud Spanner. + * @param string $projectId The project ID. + * @param string $instance The instance name. + * @param string $database The database name. + * @param string $name The session name. + */ + public function __construct(ConnectionInterface $connection, $projectId, $instance, $database, $name) + { + $this->connection = $connection; + $this->projectId = $projectId; + $this->instance = $instance; + $this->database = $database; + $this->name = $name; + } + + /** + * Return info on the session + * + * Example: + * ``` + * print_r($session->info()); + * ``` + * + * @return array An array containing the `projectId`, `instance`, `database` and session `name` keys. + */ + public function info() + { + return [ + 'projectId' => $this->projectId, + 'instance' => $this->instance, + 'database' => $this->database, + 'name' => $this->name + ]; + } + + /** + * Check if the session exists. + * + * Example: + * ``` + * if ($session->exists()) { + * echo 'The session is valid!'; + * } + * ``` + * + * @param array $options [optional] Configuration options. + * @return array + */ + public function exists(array $options = []) + { + try { + $this->connection->getSession($options + [ + 'name' => $this->name() + ]); + + return true; + } catch (NotFoundException $e) { + return false; + } + } + + /** + * Delete the session. + * + * Example: + * ``` + * $session->delete(); + * ``` + * + * @param array $options [optional] Configuration options. + * @return void + */ + public function delete(array $options = []) + { + return $this->connection->deleteSession($options + [ + 'name' => $this->name() + ]); + } + + /** + * Format the constituent parts of a session name into a fully qualified session name. + * + * @return string + */ + public function name() + { + return SpannerApi::formatSessionName( + $this->projectId, + $this->instance, + $this->database, + $this->name + ); + } +} diff --git a/src/Spanner/Session/SessionClient.php b/src/Spanner/Session/SessionClient.php new file mode 100644 index 000000000000..91501a5435d7 --- /dev/null +++ b/src/Spanner/Session/SessionClient.php @@ -0,0 +1,104 @@ +spanner(); + * + * $sessionClient = $spanner->sessionClient(); + */ +class SessionClient +{ + /** + * @var ConnectionInterface + */ + private $connection; + + /** + * @var string + */ + private $projectId; + + /** + * Create a new Session Client. + * + * @param ConnectionInterface $connection A connection to the Cloud Spanner API + * @param string $projectId The current project ID + */ + public function __construct(ConnectionInterface $connection, $projectId) + { + $this->connection = $connection; + $this->projectId = $projectId; + } + + /** + * Create a new session in the given instance and database. + * + * Example: + * ``` + * $sessionName = $sessionClient->create('test-instance', 'test-database'); + * ``` + * + * @param string $instance The simple instance name. + * @param string $database The simple database name. + * @param array $options [optional] Configuration options. + * @return Session|null If the operation succeeded, a Session object will be returned, + * otherwise null. + */ + public function create($instance, $database, array $options = []) + { + $res = $this->connection->createSession($options + [ + 'database' => SpannerApi::formatDatabaseName($this->projectId, $instance, $database) + ]); + + $session = null; + if (isset($res['name'])) { + $session = new Session( + $this->connection, + $this->projectId, + SpannerApi::parseInstanceFromSessionName($res['name']), + SpannerApi::parseDatabaseFromSessionName($res['name']), + SpannerApi::parseSessionFromSessionName($res['name']) + ); + } + + return $session; + } + + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId + ]; + } +} diff --git a/src/Spanner/Session/SessionPool.php b/src/Spanner/Session/SessionPool.php new file mode 100644 index 000000000000..40c859a64e7d --- /dev/null +++ b/src/Spanner/Session/SessionPool.php @@ -0,0 +1,48 @@ +sessionClient = $sessionClient; + } + + public function addSession(Session $session) + { + $this->sessions[] = $sessions; + } + + public function session($instance, $database, $context, array $options = []) + { + return array_rand($this->sessions); + } + + public function refreshSessions() + { + // send a request from each session to keep it alive. + } +} diff --git a/src/Spanner/Session/SessionPoolInterface.php b/src/Spanner/Session/SessionPoolInterface.php new file mode 100644 index 000000000000..28662a090ae9 --- /dev/null +++ b/src/Spanner/Session/SessionPoolInterface.php @@ -0,0 +1,26 @@ +sessionClient = $sessionClient; + } + + public function session($instance, $database, $mode, array $options = []) + { + if (!isset($this->sessions[$instance.$database.$mode])) { + $this->sessions[$instance.$database] = $this->sessionClient->create($instance, $database, $options); + } + + return $this->sessions[$instance.$database]; + } +} diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php new file mode 100644 index 000000000000..c1cf2e6633af --- /dev/null +++ b/src/Spanner/SpannerClient.php @@ -0,0 +1,302 @@ +connection = new Grpc($this->configureAuthentication($config)); + + $this->sessionClient = new SessionClient($this->connection, $this->projectId); + $this->sessionPool = new SimpleSessionPool($this->sessionClient); + } + + /** + * List all available configurations + * + * Example: + * ``` + * $configurations = $spanner->configurations(); + * ``` + * + * @todo implement pagination! + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs/list List Configs + * + * @return Generator + */ + public function configurations() + { + $res = $this->connection->listConfigs([ + 'projectId' => InstanceAdminApi::formatProjectName($this->projectId) + ]); + + if (isset($res['instanceConfigs'])) { + foreach ($res['instanceConfigs'] as $config) { + $name = InstanceAdminApi::parseInstanceConfigFromInstanceConfigName($config['name']); + yield $this->configuration($name, $config); + } + } + } + + /** + * Get a configuration by its name + * + * Example: + * ``` + * $configuration = $spanner->configuration($configurationName); + * ``` + * + * @param string $name The Configuration name. + * @param array $config [optional] The configuration details. + * @return Configuration + */ + public function configuration($name, array $config = []) + { + return new Configuration($this->connection, $this->projectId, $name, $config); + } + + /** + * Create an instance + * + * Example: + * ``` + * $instance = $spanner->createInstance($configuration, 'my-application-instance'); + * ``` + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/create Create Instance + * + * @codingStandardsIgnoreStart + * @param Configuration $config The configuration to use + * @param string $name The instance name + * @param array $options [optional] { + * Configuration options + * + * @type string $displayName **Defaults to** the value of $name. + * @type int $nodeCount **Defaults to** `1`. + * @type int $state **Defaults to** + * @type array $labels [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). + * } + * @return Instance + * @codingStandardsIgnoreEnd + */ + public function createInstance(Configuration $config, $name, array $options = []) + { + $options += [ + 'displayName' => $name, + 'nodeCount' => self::DEFAULT_NODE_COUNT, + 'state' => State::CREATING, + 'labels' => [] + ]; + + $res = $this->connection->createInstance($options + [ + 'name' => InstanceAdminApi::formatInstanceName($this->projectId, $name), + 'config' => InstanceAdminApi::formatInstanceConfigName($this->projectId, $config->name()) + ]); + + return $this->instance($name); + } + + /** + * Lazily instantiate an instance + * + * Example: + * ``` + * $instance = $spanner->instance('my-application-instance'); + * ``` + * + * @param string $name The instance name + * @return Instance + */ + public function instance($name, array $instance = []) + { + return new Instance( + $this->connection, + $this->sessionPool, + $this->projectId, + $name, + $instance + ); + } + + /** + * Connect to a database to run queries or mutations. + * + * Example: + * ``` + * $database = $spanner->connect('my-application-instance', 'my-application-database'); + * ``` + * + * @param Instance|string $instance The instance object or instance name. + * @param string $name The database name. + * @return Database + */ + public function connect($instance, $name) + { + if (is_string($instance)) { + $instance = $this->instance($instance); + } + + $database = $instance->database($name); + + return $database; + } + + /** + * List instances in the project + * + * Example: + * ``` + * $instances = $spanner->instances(); + * ``` + * + * @todo implement pagination! + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/list List Instances + * + * @param array $options [optional] Configuration options + * @return Generator + */ + public function instances(array $options = []) + { + $options += [ + 'filter' => null + ]; + + $res = $this->connection->listInstances($options + [ + 'projectId' => $this->projectId, + ]); + + if (isset($res['instances'])) { + foreach ($res['instances'] as $instance) { + yield $this->instance( + InstanceAdminApi::parseInstanceFromInstanceName($instance['name']), + $instance + ); + } + } + } + + /** + * Create a new KeySet object + * + * @param array $options [optional] { + * Configuration Options + * + * @type array $keys A list of keys + * @type KeyRange[] $ranges A list of key ranges + * @type bool $all Whether to include all keys in a table + * } + * @return KeySet + */ + public function keySet(array $options = []) + { + return new KeySet($options); + } + + /** + * Create a new KeyRange object + * + * @param array $range [optional] The key range data. + * @return KeyRange + */ + public function keyRange(array $range = []) + { + return new KeyRange($range); + } + + /** + * Get the session client + * + * Example: + * ``` + * $sessionClient = $spanner->sessionClient(); + * ``` + * + * @return SessionClient + */ + public function sessionClient() + { + return $this->sessionClient; + } +} diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php new file mode 100644 index 000000000000..638330f7a515 --- /dev/null +++ b/src/Spanner/Transaction.php @@ -0,0 +1,249 @@ +operation = $operation; + $this->session = $session; + $this->context = $context; + $this->transactionId = $transaction['id']; + $this->readTimestamp = (isset($transaction['readTimestamp'])) + ? $transaction['readTimestamp'] + : null; + } + + /** + * Enqueue an insert mutation. + * + * @param string $table The table to insert into. + * @param array $data The data to insert. + * @return void + */ + public function insert($table, array $data) + { + if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new RuntimeException( + 'Cannot perform mutations in a Read-Only Transaction' + ); + } + + $this->mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data); + } + + /** + * Enqueue an update mutation. + * + * @param string $table The table to update. + * @param array $data The data to update. + * @return void + */ + public function update($table, array $data) + { + if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new RuntimeException( + 'Cannot perform mutations in a Read-Only Transaction' + ); + } + + $this->mutations[] = $this->operation->mutation(Operation::OP_UPDATE, $table, $data); + } + + /** + * Enqueue an insert or update mutation. + * + * @param string $table The table to insert into or update. + * @param array $data The data to insert or update. + * @return void + */ + public function insertOrUpdate($table, array $data) + { + if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new RuntimeException( + 'Cannot perform mutations in a Read-Only Transaction' + ); + } + + $this->mutations[] = $this->operation->mutation(Operation::OP_INSERT_OR_UPDATE, $table, $data); + } + + /** + * Enqueue an replace mutation. + * + * @param string $table The table to replace into. + * @param array $data The data to replace. + * @return void + */ + public function replace($table, array $data) + { + if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new RuntimeException( + 'Cannot perform mutations in a Read-Only Transaction' + ); + } + + $this->mutations[] = $this->operation->mutation(Operation::OP_REPLACE, $table, $data); + } + + /** + * Enqueue an delete mutation. + * + * @param string $table The table to delete from. + * @param array $key The key of the record to be deleted. + * @return void + */ + public function delete($table, array $key) + { + if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new RuntimeException( + 'Cannot perform mutations in a Read-Only Transaction' + ); + } + + $this->mutations[] = $this->operation->deleteMutation($table, $data); + } + + /** + * Run a query. + * + * @param string $sql The query string to execute. + * @param array $options [optional] Configuration options. + * @return Result + */ + public function execute($sql, array $options = []) + { + return $this->operation->execute($this->session, $sql, [ + 'transactionId' => $this->transactionId + ] + $options); + } + + /** + * Lookup rows in a table. + * + * Note that if no KeySet is specified, all rows in a table will be + * returned. + * + * @todo is returning everything a reasonable default? + * + * @param string $table The table name. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $index The name of an index on the table. + * @type array $columns A list of column names to be returned. + * @type array $keySet A [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * } + */ + public function read($table, array $options = []) + { + return $this->operation->read($this->session, $table, [ + 'transactionId' => $this->transactionId + ] + $options); + } + + /** + * Commit all mutations in a transaction. + * + * This closes the transaction, preventing any future API calls inside it. + * + * @codingStandardsIgnoreStart + * @param array $options [optional] Configuration Options. + * @return array [Response Body](https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases.sessions/commit#response-body). + * @codingStandardsIgnoreEnd + */ + public function commit(array $options = []) + { + if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new RuntimeException('Cannot commit in a Read-Only Transaction'); + } + + return $this->operation->commit($this->session, $this->mutations, [ + 'transactionId' => $this->transactionId + ] + $options); + } + + /** + * Roll back a transaction. + * + * Rolls back a transaction, releasing any locks it holds. It is a good idea + * to call this for any transaction that includes one or more Read or + * ExecuteSql requests and ultimately decides not to commit. + * + * This closes the transaction, preventing any future API calls inside it. + * + * Rollback will NOT error if the transaction is not found or was already aborted. + * + * @param array $options [optional] Configuration Options. + * @return void + */ + public function rollback(array $options = []) + { + return $this->operation->rollback($this->session, $this->transactionId, $options); + } +} From 924a098e244bcc3d69ec6dd838fcc50df3a7f8ff Mon Sep 17 00:00:00 2001 From: Michael Bausor Date: Wed, 14 Dec 2016 12:44:36 -0800 Subject: [PATCH 004/107] Regenerate spanner --- ...seAdminApi.php => DatabaseAdminClient.php} | 309 +++++++----- .../database_admin_client_config.json | 9 +- ...ceAdminApi.php => InstanceAdminClient.php} | 462 ++++++++---------- .../instance_admin_client_config.json | 2 +- .../V1/{SpannerApi.php => SpannerClient.php} | 203 ++++---- 5 files changed, 499 insertions(+), 486 deletions(-) rename src/Spanner/Admin/Database/V1/{DatabaseAdminApi.php => DatabaseAdminClient.php} (69%) rename src/Spanner/Admin/Instance/V1/{InstanceAdminApi.php => InstanceAdminClient.php} (65%) rename src/Spanner/V1/{SpannerApi.php => SpannerClient.php} (83%) diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminApi.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php similarity index 69% rename from src/Spanner/Admin/Database/V1/DatabaseAdminApi.php rename to src/Spanner/Admin/Database/V1/DatabaseAdminClient.php index ae6da1eb2f81..2632057f67c6 100644 --- a/src/Spanner/Admin/Database/V1/DatabaseAdminApi.php +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php @@ -18,6 +18,10 @@ * This file was generated from the file * https://github.com/google/googleapis/blob/master/google/spanner/admin/database/v1/spanner_database_admin.proto * and updates to that file get reflected here through a refresh process. + * + * EXPERIMENTAL: this client library class has not yet been declared beta. This class may change + * more frequently than those which have been declared beta or 1.0, including changes which break + * backwards compatibility. */ namespace Google\Cloud\Spanner\Admin\Database\V1; @@ -34,11 +38,12 @@ use google\iam\v1\SetIamPolicyRequest; use google\iam\v1\TestIamPermissionsRequest; use google\spanner\admin\database\v1\CreateDatabaseRequest; -use google\spanner\admin\database\v1\DatabaseAdminClient; +use google\spanner\admin\database\v1\DatabaseAdminGrpcClient; use google\spanner\admin\database\v1\DropDatabaseRequest; -use google\spanner\admin\database\v1\GetDatabaseDDLRequest; +use google\spanner\admin\database\v1\GetDatabaseDdlRequest; +use google\spanner\admin\database\v1\GetDatabaseRequest; use google\spanner\admin\database\v1\ListDatabasesRequest; -use google\spanner\admin\database\v1\UpdateDatabaseRequest; +use google\spanner\admin\database\v1\UpdateDatabaseDdlRequest; /** * Service Description: Cloud Spanner Database Admin API. @@ -47,19 +52,23 @@ * list databases. It also enables updating the schema of pre-existing * databases. * + * EXPERIMENTAL: this client library class has not yet been declared beta. This class may change + * more frequently than those which have been declared beta or 1.0, including changes which break + * backwards compatibility. + * * This class provides the ability to make remote calls to the backing service through method * calls that map to API methods. Sample code to get started: * * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedName = DatabaseAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * foreach ($databaseAdminApi->listDatabases($formattedName) as $element) { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * foreach ($databaseAdminClient->listDatabases($formattedParent) as $element) { * // doThingsWith(element); * } * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` @@ -69,7 +78,7 @@ * a parse method to extract the individual identifiers contained within names that are * returned. */ -class DatabaseAdminApi +class DatabaseAdminClient { /** * The default address of the service. @@ -86,9 +95,8 @@ class DatabaseAdminApi */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _GAX_VERSION = '0.1.0'; - const _CODEGEN_NAME = 'GAPIC'; - const _CODEGEN_VERSION = '0.0.0'; + const _CODEGEN_NAME = 'gapic'; + const _CODEGEN_VERSION = '0.1.0'; private static $instanceNameTemplate; private static $databaseNameTemplate; @@ -250,8 +258,7 @@ public function __construct($options = []) 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', - 'appVersion' => self::_GAX_VERSION, - 'credentialsLoader' => null, + 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), ]; $options = array_merge($defaultOptions, $options); @@ -260,7 +267,7 @@ public function __construct($options = []) 'clientVersion' => $options['appVersion'], 'codeGenName' => self::_CODEGEN_NAME, 'codeGenVersion' => self::_CODEGEN_VERSION, - 'gaxVersion' => self::_GAX_VERSION, + 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -268,9 +275,10 @@ public function __construct($options = []) $this->descriptors = [ 'listDatabases' => $defaultDescriptors, 'createDatabase' => $defaultDescriptors, - 'updateDatabase' => $defaultDescriptors, + 'getDatabase' => $defaultDescriptors, + 'updateDatabaseDdl' => $defaultDescriptors, 'dropDatabase' => $defaultDescriptors, - 'getDatabaseDDL' => $defaultDescriptors, + 'getDatabaseDdl' => $defaultDescriptors, 'setIamPolicy' => $defaultDescriptors, 'getIamPolicy' => $defaultDescriptors, 'testIamPermissions' => $defaultDescriptors, @@ -294,14 +302,14 @@ public function __construct($options = []) $this->scopes = $options['scopes']; $createStubOptions = []; - if (!empty($options['sslCreds'])) { + if (array_key_exists('sslCreds', $options)) { $createStubOptions['sslCreds'] = $options['sslCreds']; } $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); $createDatabaseAdminStubFunction = function ($hostname, $opts) { - return new DatabaseAdminClient($hostname, $opts); + return new DatabaseAdminGrpcClient($hostname, $opts); }; $this->databaseAdminStub = $this->grpcCredentialsHelper->createStub( $createDatabaseAdminStubFunction, @@ -317,19 +325,19 @@ public function __construct($options = []) * Sample code: * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedName = DatabaseAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * foreach ($databaseAdminApi->listDatabases($formattedName) as $element) { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * foreach ($databaseAdminClient->listDatabases($formattedParent) as $element) { * // doThingsWith(element); * } * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` * - * @param string $name The project whose databases should be listed. Required. + * @param string $parent Required. The instance whose databases should be listed. * Values are of the form `projects//instances/`. * @param array $optionalArgs { * Optional. @@ -343,7 +351,7 @@ public function __construct($options = []) * If no page token is specified (the default), the first page * of values will be returned. Any page token used here must have * been generated by a previous call to the API. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -351,14 +359,14 @@ public function __construct($options = []) * is not set. * } * - * @return Google\GAX\PagedListResponse + * @return \Google\GAX\PagedListResponse * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ - public function listDatabases($name, $optionalArgs = []) + public function listDatabases($parent, $optionalArgs = []) { $request = new ListDatabasesRequest(); - $request->setName($name); + $request->setParent($parent); if (isset($optionalArgs['pageSize'])) { $request->setPageSize($optionalArgs['pageSize']); } @@ -383,27 +391,34 @@ public function listDatabases($name, $optionalArgs = []) } /** - * Creates a new Cloud Spanner database. + * Creates a new Cloud Spanner database and starts to prepare it for serving. + * The returned [long-running operation][google.longrunning.Operation] will + * have a name of the format `/operations/` and + * can be used to track preparation of the database. The + * [metadata][google.longrunning.Operation.metadata] field type is + * [CreateDatabaseMetadata][google.spanner.admin.database.v1.CreateDatabaseMetadata]. The + * [response][google.longrunning.Operation.response] field type is + * [Database][google.spanner.admin.database.v1.Database], if successful. * * Sample code: * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedName = DatabaseAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $createStatement = ""; - * $response = $databaseAdminApi->createDatabase($formattedName, $createStatement); + * $response = $databaseAdminClient->createDatabase($formattedParent, $createStatement); * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` * - * @param string $name The name of the instance that will serve the new database. + * @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 A `CREATE DATABASE` statement, which specifies the name of the - * new database. The database name must conform to the regular expression - * `[a-z][a-z0-9_\-]*[a-z0-9]` and be between 2 and 30 characters in length. + * @param string $createStatement Required. A `CREATE DATABASE` statement, which specifies the ID of the + * new database. The database ID must conform to the regular expression + * `[a-z][a-z0-9_\-]*[a-z0-9]` and be between 2 and 30 characters in length. * @param array $optionalArgs { * Optional. * @@ -412,7 +427,7 @@ public function listDatabases($name, $optionalArgs = []) * database. Statements can create tables, indexes, etc. These * statements execute atomically with the creation of the database: * if there is an error in any statement, the database is not created. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -420,14 +435,14 @@ public function listDatabases($name, $optionalArgs = []) * is not set. * } * - * @return google\spanner\admin\database\v1\Database + * @return \google\longrunning\Operation * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ - public function createDatabase($name, $createStatement, $optionalArgs = []) + public function createDatabase($parent, $createStatement, $optionalArgs = []) { $request = new CreateDatabaseRequest(); - $request->setName($name); + $request->setParent($parent); $request->setCreateStatement($createStatement); if (isset($optionalArgs['extraStatements'])) { foreach ($optionalArgs['extraStatements'] as $elem) { @@ -451,27 +466,84 @@ public function createDatabase($name, $createStatement, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * Gets the state of a Cloud Spanner database. + * + * Sample code: + * ``` + * try { + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedName = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminClient->getDatabase($formattedName); + * } finally { + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); + * } + * } + * ``` + * + * @param string $name Required. The name of the requested database. Values are of the form + * `projects//instances//databases/`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\spanner\admin\database\v1\Database + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function getDatabase($name, $optionalArgs = []) + { + $request = new GetDatabaseRequest(); + $request->setName($name); + + $mergedSettings = $this->defaultCallSettings['getDatabase']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->databaseAdminStub, + 'GetDatabase', + $mergedSettings, + $this->descriptors['getDatabase'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Updates the schema of a Cloud Spanner database by - * creating/altering/dropping tables, columns, indexes, etc. The - * [UpdateDatabaseMetadata][google.spanner.admin.database.v1.UpdateDatabaseMetadata] message is used for operation - * metadata; The operation has no response. + * creating/altering/dropping tables, columns, indexes, etc. The returned + * [long-running operation][google.longrunning.Operation] will have a name of + * the format `/operations/` and can be used to + * track execution of the schema change(s). The + * [metadata][google.longrunning.Operation.metadata] field type is + * [UpdateDatabaseDdlMetadata][google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata]. The operation has no response. * * Sample code: * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $statements = []; - * $response = $databaseAdminApi->updateDatabase($formattedDatabase, $statements); + * $response = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` * - * @param string $database The database to update. + * @param string $database Required. The database to update. * @param string[] $statements DDL statements to be applied to the database. * @param array $optionalArgs { * Optional. @@ -484,19 +556,19 @@ public function createDatabase($name, $createStatement, $optionalArgs = []) * * Specifying an explicit operation ID simplifies determining * whether the statements were executed in the event that the - * [UpdateDatabase][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase] call is replayed, - * or the return value is otherwise lost: the [database][google.spanner.admin.database.v1.UpdateDatabaseRequest.database] and + * [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] call is replayed, + * or the return value is otherwise lost: the [database][google.spanner.admin.database.v1.UpdateDatabaseDdlRequest.database] and * `operation_id` fields can be combined to form the * [name][google.longrunning.Operation.name] of the resulting * [longrunning.Operation][google.longrunning.Operation]: `/operations/`. * * `operation_id` should be unique within the database, and must be - * a valid identifier: `[a-zA-Z][a-zA-Z0-9_]*`. Note that + * a valid identifier: `[a-zA-Z][a-zA-Z0-9_]*`. Note that * automatically-generated operation IDs always begin with an * underscore. If the named operation already exists, - * [UpdateDatabase][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabase] returns + * [UpdateDatabaseDdl][google.spanner.admin.database.v1.DatabaseAdmin.UpdateDatabaseDdl] returns * `ALREADY_EXISTS`. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -504,13 +576,13 @@ public function createDatabase($name, $createStatement, $optionalArgs = []) * is not set. * } * - * @return google\longrunning\Operation + * @return \google\longrunning\Operation * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ - public function updateDatabase($database, $statements, $optionalArgs = []) + public function updateDatabaseDdl($database, $statements, $optionalArgs = []) { - $request = new UpdateDatabaseRequest(); + $request = new UpdateDatabaseDdlRequest(); $request->setDatabase($database); foreach ($statements as $elem) { $request->addStatements($elem); @@ -519,14 +591,14 @@ public function updateDatabase($database, $statements, $optionalArgs = []) $request->setOperationId($optionalArgs['operationId']); } - $mergedSettings = $this->defaultCallSettings['updateDatabase']->merge( + $mergedSettings = $this->defaultCallSettings['updateDatabaseDdl']->merge( new CallSettings($optionalArgs) ); $callable = ApiCallable::createApiCall( $this->databaseAdminStub, - 'UpdateDatabase', + 'UpdateDatabaseDdl', $mergedSettings, - $this->descriptors['updateDatabase'] + $this->descriptors['updateDatabaseDdl'] ); return $callable( @@ -541,21 +613,21 @@ public function updateDatabase($database, $statements, $optionalArgs = []) * Sample code: * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - * $databaseAdminApi->dropDatabase($formattedDatabase); + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $databaseAdminClient->dropDatabase($formattedDatabase); * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` * - * @param string $database The database to be dropped. + * @param string $database Required. The database to be dropped. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -563,7 +635,7 @@ public function updateDatabase($database, $statements, $optionalArgs = []) * is not set. * } * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function dropDatabase($database, $optionalArgs = []) { @@ -594,21 +666,21 @@ public function dropDatabase($database, $optionalArgs = []) * Sample code: * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedDatabase = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - * $response = $databaseAdminApi->getDatabaseDDL($formattedDatabase); + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminClient->getDatabaseDdl($formattedDatabase); * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` * - * @param string $database The database whose schema we wish to get. + * @param string $database Required. The database whose schema we wish to get. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -616,23 +688,23 @@ public function dropDatabase($database, $optionalArgs = []) * is not set. * } * - * @return google\spanner\admin\database\v1\GetDatabaseDDLResponse + * @return \google\spanner\admin\database\v1\GetDatabaseDdlResponse * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ - public function getDatabaseDDL($database, $optionalArgs = []) + public function getDatabaseDdl($database, $optionalArgs = []) { - $request = new GetDatabaseDDLRequest(); + $request = new GetDatabaseDdlRequest(); $request->setDatabase($database); - $mergedSettings = $this->defaultCallSettings['getDatabaseDDL']->merge( + $mergedSettings = $this->defaultCallSettings['getDatabaseDdl']->merge( new CallSettings($optionalArgs) ); $callable = ApiCallable::createApiCall( $this->databaseAdminStub, - 'GetDatabaseDDL', + 'GetDatabaseDdl', $mergedSettings, - $this->descriptors['getDatabaseDDL'] + $this->descriptors['getDatabaseDdl'] ); return $callable( @@ -645,16 +717,19 @@ public function getDatabaseDDL($database, $optionalArgs = []) * Sets the access control policy on a database resource. Replaces any * existing policy. * + * Authorization requires `spanner.databases.setIamPolicy` permission on + * [resource][google.iam.v1.SetIamPolicyRequest.resource]. + * * Sample code: * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $policy = new Policy(); - * $response = $databaseAdminApi->setIamPolicy($formattedResource, $policy); + * $response = $databaseAdminClient->setIamPolicy($formattedResource, $policy); * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` @@ -669,7 +744,7 @@ public function getDatabaseDDL($database, $optionalArgs = []) * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -677,9 +752,9 @@ public function getDatabaseDDL($database, $optionalArgs = []) * is not set. * } * - * @return google\iam\v1\Policy + * @return \google\iam\v1\Policy * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function setIamPolicy($resource, $policy, $optionalArgs = []) { @@ -707,15 +782,18 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * Gets the access control policy for a database resource. Returns an empty * policy if a database exists but does not have a policy set. * + * Authorization requires `spanner.databases.getIamPolicy` permission on + * [resource][google.iam.v1.GetIamPolicyRequest.resource]. + * * Sample code: * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - * $response = $databaseAdminApi->getIamPolicy($formattedResource); + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $databaseAdminClient->getIamPolicy($formattedResource); * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` @@ -726,7 +804,7 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -734,9 +812,9 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * is not set. * } * - * @return google\iam\v1\Policy + * @return \google\iam\v1\Policy * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function getIamPolicy($resource, $optionalArgs = []) { @@ -762,16 +840,21 @@ public function getIamPolicy($resource, $optionalArgs = []) /** * Returns permissions that the caller has on the specified database resource. * + * Attempting this RPC on a non-existent Cloud Spanner database will result in + * a NOT_FOUND error if the user has `spanner.databases.list` permission on + * the containing Cloud Spanner instance. Otherwise returns an empty set of + * permissions. + * * Sample code: * ``` * try { - * $databaseAdminApi = new DatabaseAdminApi(); - * $formattedResource = DatabaseAdminApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $databaseAdminClient = new DatabaseAdminClient(); + * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $permissions = []; - * $response = $databaseAdminApi->testIamPermissions($formattedResource, $permissions); + * $response = $databaseAdminClient->testIamPermissions($formattedResource, $permissions); * } finally { - * if (isset($databaseAdminApi)) { - * $databaseAdminApi->close(); + * if (isset($databaseAdminClient)) { + * $databaseAdminClient->close(); * } * } * ``` @@ -780,13 +863,13 @@ public function getIamPolicy($resource, $optionalArgs = []) * `resource` is usually specified as a path. For example, a Project * resource is specified as `projects/{project}`. * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with - * wildcards (such as '*' or 'storage.*') are not allowed. For more + * wildcards (such as '*' or 'storage.*') are not allowed. For more * information see * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -794,9 +877,9 @@ public function getIamPolicy($resource, $optionalArgs = []) * is not set. * } * - * @return google\iam\v1\TestIamPermissionsResponse + * @return \google\iam\v1\TestIamPermissionsResponse * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function testIamPermissions($resource, $permissions, $optionalArgs = []) { diff --git a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json index b1a17f9f00c7..16f75e93befb 100644 --- a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json +++ b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json @@ -32,7 +32,12 @@ "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, - "UpdateDatabase": { + "GetDatabase": { + "timeout_millis": 30000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "UpdateDatabaseDdl": { "timeout_millis": 30000, "retry_codes_name": "idempotent", "retry_params_name": "default" @@ -42,7 +47,7 @@ "retry_codes_name": "idempotent", "retry_params_name": "default" }, - "GetDatabaseDDL": { + "GetDatabaseDdl": { "timeout_millis": 30000, "retry_codes_name": "idempotent", "retry_params_name": "default" diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminApi.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php similarity index 65% rename from src/Spanner/Admin/Instance/V1/InstanceAdminApi.php rename to src/Spanner/Admin/Instance/V1/InstanceAdminClient.php index f38aff05a78e..e60ef9b14714 100644 --- a/src/Spanner/Admin/Instance/V1/InstanceAdminApi.php +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -18,6 +18,10 @@ * This file was generated from the file * https://github.com/google/googleapis/blob/master/google/spanner/admin/instance/v1/spanner_instance_admin.proto * and updates to that file get reflected here through a refresh process. + * + * EXPERIMENTAL: this client library class has not yet been declared beta. This class may change + * more frequently than those which have been declared beta or 1.0, including changes which break + * backwards compatibility. */ namespace Google\Cloud\Spanner\Admin\Instance\V1; @@ -33,15 +37,16 @@ use google\iam\v1\Policy; use google\iam\v1\SetIamPolicyRequest; use google\iam\v1\TestIamPermissionsRequest; +use google\protobuf\FieldMask; +use google\spanner\admin\instance\v1\CreateInstanceRequest; use google\spanner\admin\instance\v1\DeleteInstanceRequest; use google\spanner\admin\instance\v1\GetInstanceConfigRequest; use google\spanner\admin\instance\v1\GetInstanceRequest; use google\spanner\admin\instance\v1\Instance; -use google\spanner\admin\instance\v1\InstanceAdminClient; -use google\spanner\admin\instance\v1\Instance\LabelsEntry; -use google\spanner\admin\instance\v1\Instance\State; +use google\spanner\admin\instance\v1\InstanceAdminGrpcClient; use google\spanner\admin\instance\v1\ListInstanceConfigsRequest; use google\spanner\admin\instance\v1\ListInstancesRequest; +use google\spanner\admin\instance\v1\UpdateInstanceRequest; /** * Service Description: Cloud Spanner Instance Admin API. @@ -66,19 +71,23 @@ * instance resources, fewer resources are available for other * databases in that instance, and their performance may suffer. * + * EXPERIMENTAL: this client library class has not yet been declared beta. This class may change + * more frequently than those which have been declared beta or 1.0, including changes which break + * backwards compatibility. + * * This class provides the ability to make remote calls to the backing service through method * calls that map to API methods. Sample code to get started: * * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedName = InstanceAdminApi::formatProjectName("[PROJECT]"); - * foreach ($instanceAdminApi->listInstanceConfigs($formattedName) as $element) { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); + * foreach ($instanceAdminClient->listInstanceConfigs($formattedParent) as $element) { * // doThingsWith(element); * } * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` @@ -88,7 +97,7 @@ * a parse method to extract the individual identifiers contained within names that are * returned. */ -class InstanceAdminApi +class InstanceAdminClient { /** * The default address of the service. @@ -105,9 +114,8 @@ class InstanceAdminApi */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _GAX_VERSION = '0.1.0'; - const _CODEGEN_NAME = 'GAPIC'; - const _CODEGEN_VERSION = '0.0.0'; + const _CODEGEN_NAME = 'gapic'; + const _CODEGEN_VERSION = '0.1.0'; private static $projectNameTemplate; private static $instanceConfigNameTemplate; @@ -165,7 +173,7 @@ public static function parseProjectFromProjectName($projectName) /** * Parses the project from the given fully-qualified path which - * represents a instanceConfig resource. + * represents a instance_config resource. */ public static function parseProjectFromInstanceConfigName($instanceConfigName) { @@ -174,7 +182,7 @@ public static function parseProjectFromInstanceConfigName($instanceConfigName) /** * Parses the instance_config from the given fully-qualified path which - * represents a instanceConfig resource. + * represents a instance_config resource. */ public static function parseInstanceConfigFromInstanceConfigName($instanceConfigName) { @@ -297,8 +305,7 @@ public function __construct($options = []) 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', - 'appVersion' => self::_GAX_VERSION, - 'credentialsLoader' => null, + 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), ]; $options = array_merge($defaultOptions, $options); @@ -307,7 +314,7 @@ public function __construct($options = []) 'clientVersion' => $options['appVersion'], 'codeGenName' => self::_CODEGEN_NAME, 'codeGenVersion' => self::_CODEGEN_VERSION, - 'gaxVersion' => self::_GAX_VERSION, + 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -343,14 +350,14 @@ public function __construct($options = []) $this->scopes = $options['scopes']; $createStubOptions = []; - if (!empty($options['sslCreds'])) { + if (array_key_exists('sslCreds', $options)) { $createStubOptions['sslCreds'] = $options['sslCreds']; } $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); $createInstanceAdminStubFunction = function ($hostname, $opts) { - return new InstanceAdminClient($hostname, $opts); + return new InstanceAdminGrpcClient($hostname, $opts); }; $this->instanceAdminStub = $this->grpcCredentialsHelper->createStub( $createInstanceAdminStubFunction, @@ -366,19 +373,19 @@ public function __construct($options = []) * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedName = InstanceAdminApi::formatProjectName("[PROJECT]"); - * foreach ($instanceAdminApi->listInstanceConfigs($formattedName) as $element) { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); + * foreach ($instanceAdminClient->listInstanceConfigs($formattedParent) as $element) { * // doThingsWith(element); * } * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` * - * @param string $name The name of the project for which a list of supported instance + * @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 { @@ -393,7 +400,7 @@ public function __construct($options = []) * If no page token is specified (the default), the first page * of values will be returned. Any page token used here must have * been generated by a previous call to the API. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -401,14 +408,14 @@ public function __construct($options = []) * is not set. * } * - * @return Google\GAX\PagedListResponse + * @return \Google\GAX\PagedListResponse * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ - public function listInstanceConfigs($name, $optionalArgs = []) + public function listInstanceConfigs($parent, $optionalArgs = []) { $request = new ListInstanceConfigsRequest(); - $request->setName($name); + $request->setParent($parent); if (isset($optionalArgs['pageSize'])) { $request->setPageSize($optionalArgs['pageSize']); } @@ -438,22 +445,22 @@ public function listInstanceConfigs($name, $optionalArgs = []) * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedName = InstanceAdminApi::formatInstanceConfigName("[PROJECT]", "[INSTANCE_CONFIG]"); - * $response = $instanceAdminApi->getInstanceConfig($formattedName); + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedName = InstanceAdminClient::formatInstanceConfigName("[PROJECT]", "[INSTANCE_CONFIG]"); + * $response = $instanceAdminClient->getInstanceConfig($formattedName); * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` * - * @param string $name The name of the requested instance configuration. Values are of the form - * `projects//instanceConfigs/`. + * @param string $name Required. The name of the requested instance configuration. Values are of + * the form `projects//instanceConfigs/`. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -461,9 +468,9 @@ public function listInstanceConfigs($name, $optionalArgs = []) * is not set. * } * - * @return google\spanner\admin\instance\v1\InstanceConfig + * @return \google\spanner\admin\instance\v1\InstanceConfig * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function getInstanceConfig($name, $optionalArgs = []) { @@ -492,19 +499,19 @@ public function getInstanceConfig($name, $optionalArgs = []) * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedName = InstanceAdminApi::formatProjectName("[PROJECT]"); - * foreach ($instanceAdminApi->listInstances($formattedName) as $element) { + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); + * foreach ($instanceAdminClient->listInstances($formattedParent) as $element) { * // doThingsWith(element); * } * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` * - * @param string $name The name of the project for which a list of instances is + * @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. @@ -522,21 +529,21 @@ public function getInstanceConfig($name, $optionalArgs = []) * 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 + * * 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 is howl. - * * name:HOWL --> Equivalent to above. - * * NAME:howl --> Equivalent to above. - * * labels.env:* --> The instance has the label env. - * * labels.env:dev --> The instance's label env has the value dev. - * * name:howl labels.env:dev --> The instance's name is howl and it has + * * name:* --> The instance has a name. + * * name:Howl --> The instance's name is howl. + * * name:HOWL --> Equivalent to above. + * * NAME:howl --> Equivalent to above. + * * labels.env:* --> The instance has the label env. + * * labels.env:dev --> The instance's label env has the value dev. + * * name:howl labels.env:dev --> The instance's name is howl and it has * the label env with value dev. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -544,14 +551,14 @@ public function getInstanceConfig($name, $optionalArgs = []) * is not set. * } * - * @return Google\GAX\PagedListResponse + * @return \Google\GAX\PagedListResponse * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ - public function listInstances($name, $optionalArgs = []) + public function listInstances($parent, $optionalArgs = []) { $request = new ListInstancesRequest(); - $request->setName($name); + $request->setParent($parent); if (isset($optionalArgs['pageSize'])) { $request->setPageSize($optionalArgs['pageSize']); } @@ -584,22 +591,22 @@ public function listInstances($name, $optionalArgs = []) * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * $response = $instanceAdminApi->getInstance($formattedName); + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $response = $instanceAdminClient->getInstance($formattedName); * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` * - * @param string $name The name of the requested instance. Values are of the form + * @param string $name Required. The name of the requested instance. Values are of the form * `projects//instances/`. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -607,9 +614,9 @@ public function listInstances($name, $optionalArgs = []) * is not set. * } * - * @return google\spanner\admin\instance\v1\Instance + * @return \google\spanner\admin\instance\v1\Instance * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function getInstance($name, $optionalArgs = []) { @@ -642,94 +649,58 @@ public function getInstance($name, $optionalArgs = []) * * Immediately upon completion of this request: * - * * The instance is readable via the API, with all requested attributes + * * 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 + * * 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. + * * 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 + * * 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`. + * * 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 operation's + * The returned [long-running operation][google.longrunning.Operation] will + * have a name of the format `/operations/` and + * can be used to track creation of the instance. The * [metadata][google.longrunning.Operation.metadata] field type is - * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata] - * The returned operation's - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if - * successful. - * - * Authorization requires `spanner.instances.create` permission on - * resource [name][google.spanner.admin.instance.v1.Instance.name]. + * [CreateInstanceMetadata][google.spanner.admin.instance.v1.CreateInstanceMetadata]. + * The [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. * * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * $config = ""; - * $displayName = ""; - * $nodeCount = 0; - * $response = $instanceAdminApi->createInstance($formattedName, $config, $displayName, $nodeCount); + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); + * $instanceId = ""; + * $instance = new Instance(); + * $response = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` * - * @param string $name A unique identifier for the instance, which cannot be changed after - * the instance is created. Values are of the form - * `projects//instances/[a-z][-a-z0-9]*[a-z0-9]`. The final - * segment of the name must be between 6 and 30 characters in length. - * @param string $config The name of the instance's configuration. Values are of the form - * `projects//instanceConfigs/`. See - * also [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig] and - * [ListInstanceConfigs][google.spanner.admin.instance.v1.InstanceAdmin.ListInstanceConfigs]. - * @param string $displayName The descriptive name for this instance as it appears in UIs. - * Must be unique per project and between 4 and 30 characters in length. - * @param int $nodeCount The number of nodes allocated to this instance. - * @param array $optionalArgs { - * Optional. + * @param string $parent Required. The name of the project in which to create the instance. Values + * are of the form `projects/`. + * @param string $instanceId Required. The ID of the instance to create. Valid identifiers are of the + * form `[a-z][-a-z0-9]*[a-z0-9]` and must be between 6 and 30 characters in + * length. + * @param Instance $instance Required. The instance to create. The name may be omitted, but if + * specified must be `/instances/`. + * @param array $optionalArgs { + * Optional. * - * @type State $state - * The current instance state. For - * [CreateInstance][google.spanner.admin.instance.v1.InstanceAdmin.CreateInstance], the state must be - * either omitted or set to `CREATING`. For - * [UpdateInstance][google.spanner.admin.instance.v1.InstanceAdmin.UpdateInstance], the state must be - * either omitted or set to `READY`. - * @type array $labels - * Cloud Labels are a flexible and lightweight mechanism for organizing cloud - * resources into groups that reflect a customer's organizational needs and - * deployment strategies. Cloud Labels can be used to filter collections of - * resources. They can be used to control how resource metrics are aggregated. - * And they can be used as arguments to policy management rules (e.g. route, - * firewall, load balancing, etc.). - * - * * Label keys must be between 1 and 63 characters long and must conform to - * the following regular expression: `[a-z]([-a-z0-9]*[a-z0-9])?`. - * * Label values must be between 0 and 63 characters long and must conform - * to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. - * * No more than 64 labels can be associated with a given resource. - * - * See https://goo.gl/xmQnxf for more information on and examples of labels. - * - * If you plan to use labels in your own code, please note that additional - * characters may be allowed in the future. And so you are advised to use an - * internal label representation, such as JSON, which doesn't rely upon - * specific characters being disallowed. For example, representing labels - * as the string: name + "_" + value would prove problematic if we were to - * allow "_" in a future release. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -737,25 +708,16 @@ public function getInstance($name, $optionalArgs = []) * is not set. * } * - * @return google\longrunning\Operation + * @return \google\longrunning\Operation * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ - public function createInstance($name, $config, $displayName, $nodeCount, $optionalArgs = []) + public function createInstance($parent, $instanceId, $instance, $optionalArgs = []) { - $request = new Instance(); - $request->setName($name); - $request->setConfig($config); - $request->setDisplayName($displayName); - $request->setNodeCount($nodeCount); - if (isset($optionalArgs['state'])) { - $request->setState($optionalArgs['state']); - } - if (isset($optionalArgs['labels'])) { - foreach ($optionalArgs['labels'] as $key => $value) { - $request->addLabels((new LabelsEntry())->setKey($key)->setValue($value)); - } - } + $request = new CreateInstanceRequest(); + $request->setParent($parent); + $request->setInstanceId($instanceId); + $request->setInstance($instance); $mergedSettings = $this->defaultCallSettings['createInstance']->merge( new CallSettings($optionalArgs) @@ -782,35 +744,35 @@ public function createInstance($name, $config, $displayName, $nodeCount, $option * * Immediately upon completion of this request: * - * * For resource types for which a decrease in the instance's allocation + * * 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 + * * 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 + * * 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 + * * 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 + * * All newly-reserved resources are available for serving the instance's * tables. - * * The instance's new resource levels are readable via the API. + * * The instance's new resource levels are readable via the API. * - * The returned operation's + * The returned [long-running operation][google.longrunning.Operation] will + * have a name of the format `/operations/` and + * can be used to track the instance modification. The * [metadata][google.longrunning.Operation.metadata] field type is - * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata] - * The returned operation's - * [response][google.longrunning.Operation.response] field type is - * [Instance][google.spanner.admin.instance.v1.Instance], if - * successful. + * [UpdateInstanceMetadata][google.spanner.admin.instance.v1.UpdateInstanceMetadata]. + * The [response][google.longrunning.Operation.response] field type is + * [Instance][google.spanner.admin.instance.v1.Instance], if successful. * * Authorization requires `spanner.instances.update` permission on * resource [name][google.spanner.admin.instance.v1.Instance.name]. @@ -818,62 +780,27 @@ public function createInstance($name, $config, $displayName, $nodeCount, $option * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * $config = ""; - * $displayName = ""; - * $nodeCount = 0; - * $response = $instanceAdminApi->updateInstance($formattedName, $config, $displayName, $nodeCount); + * $instanceAdminClient = new InstanceAdminClient(); + * $instance = new Instance(); + * $fieldMask = new FieldMask(); + * $response = $instanceAdminClient->updateInstance($instance, $fieldMask); * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` * - * @param string $name A unique identifier for the instance, which cannot be changed after - * the instance is created. Values are of the form - * `projects//instances/[a-z][-a-z0-9]*[a-z0-9]`. The final - * segment of the name must be between 6 and 30 characters in length. - * @param string $config The name of the instance's configuration. Values are of the form - * `projects//instanceConfigs/`. See - * also [InstanceConfig][google.spanner.admin.instance.v1.InstanceConfig] and - * [ListInstanceConfigs][google.spanner.admin.instance.v1.InstanceAdmin.ListInstanceConfigs]. - * @param string $displayName The descriptive name for this instance as it appears in UIs. - * Must be unique per project and between 4 and 30 characters in length. - * @param int $nodeCount The number of nodes allocated to this instance. - * @param array $optionalArgs { - * Optional. + * @param Instance $instance Required. The instance to update, which must always include the instance + * name. Otherwise, only fields mentioned in [][google.spanner.admin.instance.v1.UpdateInstanceRequest.field_mask] need be included. + * @param FieldMask $fieldMask Required. A mask specifying which fields in [][google.spanner.admin.instance.v1.UpdateInstanceRequest.instance] should be updated. + * The field mask must always be specified; this prevents any future fields in + * [][google.spanner.admin.instance.v1.Instance] from being erased accidentally by clients that do not know + * about them. + * @param array $optionalArgs { + * Optional. * - * @type State $state - * The current instance state. For - * [CreateInstance][google.spanner.admin.instance.v1.InstanceAdmin.CreateInstance], the state must be - * either omitted or set to `CREATING`. For - * [UpdateInstance][google.spanner.admin.instance.v1.InstanceAdmin.UpdateInstance], the state must be - * either omitted or set to `READY`. - * @type array $labels - * Cloud Labels are a flexible and lightweight mechanism for organizing cloud - * resources into groups that reflect a customer's organizational needs and - * deployment strategies. Cloud Labels can be used to filter collections of - * resources. They can be used to control how resource metrics are aggregated. - * And they can be used as arguments to policy management rules (e.g. route, - * firewall, load balancing, etc.). - * - * * Label keys must be between 1 and 63 characters long and must conform to - * the following regular expression: `[a-z]([-a-z0-9]*[a-z0-9])?`. - * * Label values must be between 0 and 63 characters long and must conform - * to the regular expression `([a-z]([-a-z0-9]*[a-z0-9])?)?`. - * * No more than 64 labels can be associated with a given resource. - * - * See https://goo.gl/xmQnxf for more information on and examples of labels. - * - * If you plan to use labels in your own code, please note that additional - * characters may be allowed in the future. And so you are advised to use an - * internal label representation, such as JSON, which doesn't rely upon - * specific characters being disallowed. For example, representing labels - * as the string: name + "_" + value would prove problematic if we were to - * allow "_" in a future release. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -881,25 +808,15 @@ public function createInstance($name, $config, $displayName, $nodeCount, $option * is not set. * } * - * @return google\longrunning\Operation + * @return \google\longrunning\Operation * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ - public function updateInstance($name, $config, $displayName, $nodeCount, $optionalArgs = []) + public function updateInstance($instance, $fieldMask, $optionalArgs = []) { - $request = new Instance(); - $request->setName($name); - $request->setConfig($config); - $request->setDisplayName($displayName); - $request->setNodeCount($nodeCount); - if (isset($optionalArgs['state'])) { - $request->setState($optionalArgs['state']); - } - if (isset($optionalArgs['labels'])) { - foreach ($optionalArgs['labels'] as $key => $value) { - $request->addLabels((new LabelsEntry())->setKey($key)->setValue($value)); - } - } + $request = new UpdateInstanceRequest(); + $request->setInstance($instance); + $request->setFieldMask($fieldMask); $mergedSettings = $this->defaultCallSettings['updateInstance']->merge( new CallSettings($optionalArgs) @@ -922,33 +839,33 @@ public function updateInstance($name, $config, $displayName, $nodeCount, $option * * Immediately upon completion of the request: * - * * Billing ceases for all of the instance's reserved resources. + * * Billing ceases for all of the instance's reserved resources. * * Soon afterward: * - * * The instance and *all of its databases* immediately and + * * The instance and *all of its databases* immediately and * irrevocably disappear from the API. All data in the databases * is permanently deleted. * * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedName = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * $instanceAdminApi->deleteInstance($formattedName); + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $instanceAdminClient->deleteInstance($formattedName); * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` * - * @param string $name The name of the instance to be deleted. Values are of the form + * @param string $name Required. The name of the instance to be deleted. Values are of the form * `projects//instances/` * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -956,7 +873,7 @@ public function updateInstance($name, $config, $displayName, $nodeCount, $option * is not set. * } * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function deleteInstance($name, $optionalArgs = []) { @@ -983,16 +900,19 @@ public function deleteInstance($name, $optionalArgs = []) * Sets the access control policy on an instance resource. Replaces any * existing policy. * + * Authorization requires `spanner.instances.setIamPolicy` on + * [resource][google.iam.v1.SetIamPolicyRequest.resource]. + * * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $policy = new Policy(); - * $response = $instanceAdminApi->setIamPolicy($formattedResource, $policy); + * $response = $instanceAdminClient->setIamPolicy($formattedResource, $policy); * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` @@ -1007,7 +927,7 @@ public function deleteInstance($name, $optionalArgs = []) * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -1015,9 +935,9 @@ public function deleteInstance($name, $optionalArgs = []) * is not set. * } * - * @return google\iam\v1\Policy + * @return \google\iam\v1\Policy * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function setIamPolicy($resource, $policy, $optionalArgs = []) { @@ -1045,15 +965,18 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * Gets the access control policy for an instance resource. Returns an empty * policy if an instance exists but does not have a policy set. * + * Authorization requires `spanner.instances.getIamPolicy` on + * [resource][google.iam.v1.GetIamPolicyRequest.resource]. + * * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * $response = $instanceAdminApi->getIamPolicy($formattedResource); + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $response = $instanceAdminClient->getIamPolicy($formattedResource); * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` @@ -1064,7 +987,7 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -1072,9 +995,9 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * is not set. * } * - * @return google\iam\v1\Policy + * @return \google\iam\v1\Policy * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function getIamPolicy($resource, $optionalArgs = []) { @@ -1100,16 +1023,21 @@ public function getIamPolicy($resource, $optionalArgs = []) /** * Returns permissions that the caller has on the specified instance resource. * + * Attempting this RPC on a non-existent Cloud Spanner instance resource will + * result in a NOT_FOUND error if the user has `spanner.instances.list` + * permission on the containing Google Cloud Project. Otherwise returns an + * empty set of permissions. + * * Sample code: * ``` * try { - * $instanceAdminApi = new InstanceAdminApi(); - * $formattedResource = InstanceAdminApi::formatInstanceName("[PROJECT]", "[INSTANCE]"); + * $instanceAdminClient = new InstanceAdminClient(); + * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $permissions = []; - * $response = $instanceAdminApi->testIamPermissions($formattedResource, $permissions); + * $response = $instanceAdminClient->testIamPermissions($formattedResource, $permissions); * } finally { - * if (isset($instanceAdminApi)) { - * $instanceAdminApi->close(); + * if (isset($instanceAdminClient)) { + * $instanceAdminClient->close(); * } * } * ``` @@ -1118,13 +1046,13 @@ public function getIamPolicy($resource, $optionalArgs = []) * `resource` is usually specified as a path. For example, a Project * resource is specified as `projects/{project}`. * @param string[] $permissions The set of permissions to check for the `resource`. Permissions with - * wildcards (such as '*' or 'storage.*') are not allowed. For more + * wildcards (such as '*' or 'storage.*') are not allowed. For more * information see * [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions). * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -1132,9 +1060,9 @@ public function getIamPolicy($resource, $optionalArgs = []) * is not set. * } * - * @return google\iam\v1\TestIamPermissionsResponse + * @return \google\iam\v1\TestIamPermissionsResponse * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function testIamPermissions($resource, $permissions, $optionalArgs = []) { diff --git a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json index 2aa03b47e61d..6771a7e9d440 100644 --- a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json +++ b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json @@ -44,7 +44,7 @@ }, "CreateInstance": { "timeout_millis": 30000, - "retry_codes_name": "idempotent", + "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, "UpdateInstance": { diff --git a/src/Spanner/V1/SpannerApi.php b/src/Spanner/V1/SpannerClient.php similarity index 83% rename from src/Spanner/V1/SpannerApi.php rename to src/Spanner/V1/SpannerClient.php index 4607be193f4d..45c6bbb66631 100644 --- a/src/Spanner/V1/SpannerApi.php +++ b/src/Spanner/V1/SpannerClient.php @@ -18,6 +18,10 @@ * This file was generated from the file * https://github.com/google/googleapis/blob/master/google/spanner/v1/spanner.proto * and updates to that file get reflected here through a refresh process. + * + * EXPERIMENTAL: this client library class has not yet been declared beta. This class may change + * more frequently than those which have been declared beta or 1.0, including changes which break + * backwards compatibility. */ namespace Google\Cloud\Spanner\V1; @@ -41,7 +45,7 @@ use google\spanner\v1\Mutation; use google\spanner\v1\ReadRequest; use google\spanner\v1\RollbackRequest; -use google\spanner\v1\SpannerClient; +use google\spanner\v1\SpannerGrpcClient; use google\spanner\v1\TransactionOptions; use google\spanner\v1\TransactionSelector; @@ -51,17 +55,21 @@ * The Cloud Spanner API can be used to manage sessions and execute * transactions on data stored in Cloud Spanner databases. * + * EXPERIMENTAL: this client library class has not yet been declared beta. This class may change + * more frequently than those which have been declared beta or 1.0, including changes which break + * backwards compatibility. + * * This class provides the ability to make remote calls to the backing service through method * calls that map to API methods. Sample code to get started: * * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedDatabase = SpannerApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - * $response = $spannerApi->createSession($formattedDatabase); + * $spannerClient = new SpannerClient(); + * $formattedDatabase = SpannerClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $spannerClient->createSession($formattedDatabase); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` @@ -71,7 +79,7 @@ * a parse method to extract the individual identifiers contained within names that are * returned. */ -class SpannerApi +class SpannerClient { /** * The default address of the service. @@ -88,9 +96,8 @@ class SpannerApi */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _GAX_VERSION = '0.1.0'; - const _CODEGEN_NAME = 'GAPIC'; - const _CODEGEN_VERSION = '0.0.0'; + const _CODEGEN_NAME = 'gapic'; + const _CODEGEN_VERSION = '0.1.0'; private static $databaseNameTemplate; private static $sessionNameTemplate; @@ -263,8 +270,7 @@ public function __construct($options = []) 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', - 'appVersion' => self::_GAX_VERSION, - 'credentialsLoader' => null, + 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), ]; $options = array_merge($defaultOptions, $options); @@ -273,7 +279,7 @@ public function __construct($options = []) 'clientVersion' => $options['appVersion'], 'codeGenName' => self::_CODEGEN_NAME, 'codeGenVersion' => self::_CODEGEN_VERSION, - 'gaxVersion' => self::_GAX_VERSION, + 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -307,14 +313,14 @@ public function __construct($options = []) $this->scopes = $options['scopes']; $createStubOptions = []; - if (!empty($options['sslCreds'])) { + if (array_key_exists('sslCreds', $options)) { $createStubOptions['sslCreds'] = $options['sslCreds']; } $grpcCredentialsHelperOptions = array_diff_key($options, $defaultOptions); $this->grpcCredentialsHelper = new GrpcCredentialsHelper($this->scopes, $grpcCredentialsHelperOptions); $createSpannerStubFunction = function ($hostname, $opts) { - return new SpannerClient($hostname, $opts); + return new SpannerGrpcClient($hostname, $opts); }; $this->spannerStub = $this->grpcCredentialsHelper->createStub( $createSpannerStubFunction, @@ -349,21 +355,21 @@ public function __construct($options = []) * Sample code: * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedDatabase = SpannerApi::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); - * $response = $spannerApi->createSession($formattedDatabase); + * $spannerClient = new SpannerClient(); + * $formattedDatabase = SpannerClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); + * $response = $spannerClient->createSession($formattedDatabase); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` * - * @param string $database The database in which the new session is created. + * @param string $database Required. The database in which the new session is created. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -371,9 +377,9 @@ public function __construct($options = []) * is not set. * } * - * @return google\spanner\v1\Session + * @return \google\spanner\v1\Session * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function createSession($database, $optionalArgs = []) { @@ -404,21 +410,21 @@ public function createSession($database, $optionalArgs = []) * Sample code: * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedName = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); - * $response = $spannerApi->getSession($formattedName); + * $spannerClient = new SpannerClient(); + * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $response = $spannerClient->getSession($formattedName); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` * - * @param string $name The name of the session to retrieve. + * @param string $name Required. The name of the session to retrieve. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -426,9 +432,9 @@ public function createSession($database, $optionalArgs = []) * is not set. * } * - * @return google\spanner\v1\Session + * @return \google\spanner\v1\Session * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function getSession($name, $optionalArgs = []) { @@ -457,21 +463,21 @@ public function getSession($name, $optionalArgs = []) * Sample code: * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedName = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); - * $spannerApi->deleteSession($formattedName); + * $spannerClient = new SpannerClient(); + * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerClient->deleteSession($formattedName); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` * - * @param string $name The name of the session to delete. + * @param string $name Required. The name of the session to delete. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -479,7 +485,7 @@ public function getSession($name, $optionalArgs = []) * is not set. * } * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function deleteSession($name, $optionalArgs = []) { @@ -518,19 +524,19 @@ public function deleteSession($name, $optionalArgs = []) * Sample code: * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $sql = ""; - * $response = $spannerApi->executeSql($formattedSession, $sql); + * $response = $spannerClient->executeSql($formattedSession, $sql); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` * - * @param string $session The session in which the SQL query should be performed. - * @param string $sql The SQL query string. + * @param string $session Required. The session in which the SQL query should be performed. + * @param string $sql Required. The SQL query string. * @param array $optionalArgs { * Optional. * @@ -539,13 +545,13 @@ public function deleteSession($name, $optionalArgs = []) * temporary read-only transaction with strong concurrency. * @type Struct $params * The SQL query string can contain parameter placeholders. A parameter - * placeholder consists of `'@'` followed by the parameter + * placeholder consists of `'@'` followed by the parameter * name. Parameter names consist of any combination of letters, * numbers, and underscores. * * Parameters can appear anywhere that a literal value is expected. The same * parameter name can be used more than once, for example: - * `"WHERE id > @msg_id AND id < @msg_id + 100"` + * `"WHERE id > @msg_id AND id < @msg_id + 100"` * * It is an error to execute an SQL query with unbound parameters. * @@ -571,7 +577,7 @@ public function deleteSession($name, $optionalArgs = []) * @type QueryMode $queryMode * Used to control the amount of debugging information returned in * [ResultSetStats][google.spanner.v1.ResultSetStats]. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -579,9 +585,9 @@ public function deleteSession($name, $optionalArgs = []) * is not set. * } * - * @return google\spanner\v1\ResultSet + * @return \google\spanner\v1\ResultSet * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function executeSql($session, $sql, $optionalArgs = []) { @@ -640,24 +646,24 @@ public function executeSql($session, $sql, $optionalArgs = []) * Sample code: * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $table = ""; * $columns = []; * $keySet = new KeySet(); - * $response = $spannerApi->read($formattedSession, $table, $columns, $keySet); + * $response = $spannerClient->read($formattedSession, $table, $columns, $keySet); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` * - * @param string $session The session in which the read should be performed. - * @param string $table The name of the table in the database to be read. Must be non-empty. + * @param string $session Required. The session in which the read should be performed. + * @param string $table Required. The name of the table in the database to be read. * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching * this request. - * @param KeySet $keySet `key_set` identifies the rows to be yielded. `key_set` names the + * @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]. @@ -677,15 +683,9 @@ public function executeSql($session, $sql, $optionalArgs = []) * 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 $offset - * The first `offset` rows matching [key_set][google.spanner.v1.ReadRequest.key_set] are skipped. Note - * that the implementation must read the rows in order to skip - * them. Where possible, it is much more efficient to adjust [key_set][google.spanner.v1.ReadRequest.key_set] - * to exclude unwanted rows. * @type int $limit - * If greater than zero, after skipping the first [offset][google.spanner.v1.ReadRequest.offset] rows, - * only the next `limit` rows are yielded. If `limit` is zero, - * the default is no limit. + * If greater than zero, only the first `limit` rows are yielded. If `limit` + * is zero, the default is no limit. * @type string $resumeToken * If this request is resuming a previously interrupted read, * `resume_token` should be copied from the last @@ -693,7 +693,7 @@ public function executeSql($session, $sql, $optionalArgs = []) * enables the new read to resume where the last read left off. The * rest of the request parameters must exactly match the request * that yielded this token. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -701,9 +701,9 @@ public function executeSql($session, $sql, $optionalArgs = []) * is not set. * } * - * @return google\spanner\v1\ResultSet + * @return \google\spanner\v1\ResultSet * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function read($session, $table, $columns, $keySet, $optionalArgs = []) { @@ -720,9 +720,6 @@ public function read($session, $table, $columns, $keySet, $optionalArgs = []) if (isset($optionalArgs['index'])) { $request->setIndex($optionalArgs['index']); } - if (isset($optionalArgs['offset'])) { - $request->setOffset($optionalArgs['offset']); - } if (isset($optionalArgs['limit'])) { $request->setLimit($optionalArgs['limit']); } @@ -755,23 +752,23 @@ public function read($session, $table, $columns, $keySet, $optionalArgs = []) * Sample code: * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $options = new TransactionOptions(); - * $response = $spannerApi->beginTransaction($formattedSession, $options); + * $response = $spannerClient->beginTransaction($formattedSession, $options); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` * - * @param string $session The session in which the transaction runs. - * @param TransactionOptions $options Options for the new transaction. + * @param string $session Required. The session in which the transaction runs. + * @param TransactionOptions $options Required. Options for the new transaction. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -779,9 +776,9 @@ public function read($session, $table, $columns, $keySet, $optionalArgs = []) * is not set. * } * - * @return google\spanner\v1\Transaction + * @return \google\spanner\v1\Transaction * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function beginTransaction($session, $options, $optionalArgs = []) { @@ -818,18 +815,18 @@ public function beginTransaction($session, $options, $optionalArgs = []) * Sample code: * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $mutations = []; - * $response = $spannerApi->commit($formattedSession, $mutations); + * $response = $spannerClient->commit($formattedSession, $mutations); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` * - * @param string $session The session in which the transaction to be committed is running. + * @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. @@ -848,7 +845,7 @@ public function beginTransaction($session, $options, $optionalArgs = []) * executed more than once. If this is undesirable, use * [BeginTransaction][google.spanner.v1.Spanner.BeginTransaction] and * [Commit][google.spanner.v1.Spanner.Commit] instead. - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -856,9 +853,9 @@ public function beginTransaction($session, $options, $optionalArgs = []) * is not set. * } * - * @return google\spanner\v1\CommitResponse + * @return \google\spanner\v1\CommitResponse * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function commit($session, $mutations, $optionalArgs = []) { @@ -903,23 +900,23 @@ public function commit($session, $mutations, $optionalArgs = []) * Sample code: * ``` * try { - * $spannerApi = new SpannerApi(); - * $formattedSession = SpannerApi::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $transactionId = ""; - * $spannerApi->rollback($formattedSession, $transactionId); + * $spannerClient->rollback($formattedSession, $transactionId); * } finally { - * if (isset($spannerApi)) { - * $spannerApi->close(); + * if (isset($spannerClient)) { + * $spannerClient->close(); * } * } * ``` * - * @param string $session The session in which the transaction to roll back is running. - * @param string $transactionId The transaction to roll back. + * @param string $session Required. The session in which the transaction to roll back is running. + * @param string $transactionId Required. The transaction to roll back. * @param array $optionalArgs { * Optional. * - * @type Google\GAX\RetrySettings $retrySettings + * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. * @type int $timeoutMillis @@ -927,7 +924,7 @@ public function commit($session, $mutations, $optionalArgs = []) * is not set. * } * - * @throws Google\GAX\ApiException if the remote call fails + * @throws \Google\GAX\ApiException if the remote call fails */ public function rollback($session, $transactionId, $optionalArgs = []) { From 83d912c66f740a41e08faf771cb9e88028d4ae1e Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Thu, 15 Dec 2016 14:22:25 -0500 Subject: [PATCH 005/107] update to use latest gax --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index fa2e65161dd8..4e461b5e3c4e 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "require": { "php": ">=5.5", "rize/uri-template": "~0.3", - "google/auth": "0.10", + "google/auth": "^0.11", "guzzlehttp/guzzle": "^5.3|^6.0", "guzzlehttp/psr7": "^1.2", "monolog/monolog": "~1", @@ -54,8 +54,8 @@ "james-heinrich/getid3": "^1.9", "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", - "google/proto-client-php": "^0.5", - "google/gax": "^0.3" + "google/proto-client-php": "dev-master", + "google/gax": "^0.5" }, "suggest": { "google/gax": "Required to support gRPC", From d65f250b04f60c27773607425cf00e062686fdc5 Mon Sep 17 00:00:00 2001 From: Dave Supplee Date: Thu, 15 Dec 2016 14:23:15 -0500 Subject: [PATCH 006/107] rename *Api -> *Client --- src/Spanner/Configuration.php | 4 +- src/Spanner/Connection/AdminGrpc.php | 50 ++++++++-------- src/Spanner/Connection/Grpc.php | 83 +++++++++++++-------------- src/Spanner/Database.php | 4 +- src/Spanner/Instance.php | 10 ++-- src/Spanner/Session/Session.php | 4 +- src/Spanner/Session/SessionClient.php | 10 ++-- src/Spanner/SpannerClient.php | 13 ++--- tests/Spanner/ConfigurationTest.php | 6 +- tests/Spanner/DatabaseTest.php | 12 ++-- tests/Spanner/InstanceTest.php | 14 ++--- 11 files changed, 102 insertions(+), 108 deletions(-) diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php index 201832c8a526..882f729a6180 100644 --- a/src/Spanner/Configuration.php +++ b/src/Spanner/Configuration.php @@ -18,7 +18,7 @@ namespace Google\Cloud\Spanner; use Google\Cloud\Exception\NotFoundException; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminApi; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; /** @@ -156,7 +156,7 @@ public function exists(array $options = []) public function reload(array $options = []) { $this->info = $this->connection->getConfig($options + [ - 'name' => InstanceAdminApi::formatInstanceConfigName($this->projectId, $this->name), + 'name' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $this->name), 'projectId' => $this->projectId ]); diff --git a/src/Spanner/Connection/AdminGrpc.php b/src/Spanner/Connection/AdminGrpc.php index 0e8eee74785f..45c726e72768 100644 --- a/src/Spanner/Connection/AdminGrpc.php +++ b/src/Spanner/Connection/AdminGrpc.php @@ -20,8 +20,8 @@ use Google\Auth\CredentialsLoader; use Google\Cloud\GrpcRequestWrapper; use Google\Cloud\GrpcTrait; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminApi; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminApi; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\GAX\ApiException; use google\spanner\admin\instance\v1\State; @@ -29,9 +29,9 @@ class AdminGrpc implements AdminConnectionInterface { use GrpcTrait; - private $instanceAdminApi; + private $instanceAdminClient; - private $databaseAdminApi; + private $databaseAdminClient; /** * @param array $config @@ -44,8 +44,8 @@ public function __construct(array $config) $this->wrapper = new GrpcRequestWrapper; - $this->instanceAdminApi = new InstanceAdminApi($grpcConfig); - $this->databaseAdminApi = new DatabaseAdminApi($grpcConfig); + $this->instanceAdminClient = new InstanceAdminClient($grpcConfig); + $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); } /** @@ -53,7 +53,7 @@ public function __construct(array $config) */ public function listConfigs(array $args = []) { - return $this->send([$this->instanceAdminApi, 'listInstanceConfigs'], [ + return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ $args['projectId'], $args ]); @@ -64,7 +64,7 @@ public function listConfigs(array $args = []) */ public function getConfig(array $args = []) { - return $this->send([$this->instanceAdminApi, 'getInstanceConfig'], [ + return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ $args['name'], $args ]); @@ -75,8 +75,8 @@ public function getConfig(array $args = []) */ public function listInstances(array $args = []) { - return $this->send([$this->instanceAdminApi, 'listInstances'], [ - InstanceAdminApi::formatProjectName($args['projectId']), + return $this->send([$this->instanceAdminClient, 'listInstances'], [ + InstanceAdminClient::formatProjectName($args['projectId']), $args ]); } @@ -86,7 +86,7 @@ public function listInstances(array $args = []) */ public function getInstance(array $args = []) { - return $this->send([$this->instanceAdminApi, 'getInstance'], [ + return $this->send([$this->instanceAdminClient, 'getInstance'], [ $args['name'], $args ]); @@ -97,7 +97,7 @@ public function getInstance(array $args = []) */ public function createInstance(array $args = []) { - return $this->send([$this->instanceAdminApi, 'createInstance'], [ + return $this->send([$this->instanceAdminClient, 'createInstance'], [ $args['name'], $args['config'], $args['displayName'], @@ -111,7 +111,7 @@ public function createInstance(array $args = []) */ public function updateInstance(array $args = []) { - return $this->send([$this->instanceAdminApi, 'updateInstance'], [ + return $this->send([$this->instanceAdminClient, 'updateInstance'], [ $args['name'], $args['config'], $args['displayName'], @@ -127,7 +127,7 @@ public function updateInstance(array $args = []) */ public function deleteInstance(array $args = []) { - return $this->send([$this->instanceAdminApi, 'deleteInstance'], [ + return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ $args['name'], $args ]); @@ -138,7 +138,7 @@ public function deleteInstance(array $args = []) */ public function getInstanceIamPolicy(array $args = []) { - return $this->send([$this->instanceAdminApi, 'getIamPolicy'], [ + return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ $args['resource'], $args ]); @@ -149,7 +149,7 @@ public function getInstanceIamPolicy(array $args = []) */ public function setInstanceIamPolicy(array $args = []) { - return $this->send([$this->instanceAdminApi, 'setIamPolicy'], [ + return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ $args['resource'], $args['policy'], $args @@ -161,7 +161,7 @@ public function setInstanceIamPolicy(array $args = []) */ public function testInstanceIamPermissions(array $args = []) { - return $this->send([$this->instanceAdminApi, 'testIamPermissions'], [ + return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ $args['resource'], $args['permissions'], $args @@ -173,7 +173,7 @@ public function testInstanceIamPermissions(array $args = []) */ public function listDatabases(array $args = []) { - return $this->send([$this->databaseAdminApi, 'listDatabases'], [ + return $this->send([$this->databaseAdminClient, 'listDatabases'], [ $args['instance'], $args ]); @@ -184,7 +184,7 @@ public function listDatabases(array $args = []) */ public function createDatabase(array $args = []) { - return $this->send([$this->databaseAdminApi, 'createDatabase'], [ + return $this->send([$this->databaseAdminClient, 'createDatabase'], [ $args['instance'], $args['createStatement'], $args['extraStatements'], @@ -197,7 +197,7 @@ public function createDatabase(array $args = []) */ public function updateDatabase(array $args = []) { - return $this->send([$this->databaseAdminApi, 'updateDatabase'], [ + return $this->send([$this->databaseAdminClient, 'updateDatabase'], [ $args['name'], $args['statements'], $args @@ -209,7 +209,7 @@ public function updateDatabase(array $args = []) */ public function dropDatabase(array $args = []) { - return $this->send([$this->databaseAdminApi, 'dropDatabase'], [ + return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ $args['name'], $args ]); @@ -220,7 +220,7 @@ public function dropDatabase(array $args = []) */ public function getDatabaseDDL(array $args = []) { - return $this->send([$this->databaseAdminApi, 'getDatabaseDDL'], [ + return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ $args['name'], $args ]); @@ -231,7 +231,7 @@ public function getDatabaseDDL(array $args = []) */ public function getDatabaseIamPolicy(array $args = []) { - return $this->send([$this->databaseAdminApi, 'getIamPolicy'], [ + return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ $args['resource'], $args ]); @@ -242,7 +242,7 @@ public function getDatabaseIamPolicy(array $args = []) */ public function setDatabaseIamPolicy(array $args = []) { - return $this->send([$this->databaseAdminApi, 'setIamPolicy'], [ + return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ $args['resource'], $args['policy'], $args @@ -254,7 +254,7 @@ public function setDatabaseIamPolicy(array $args = []) */ public function testDatabaseIamPermissions(array $args = []) { - return $this->send([$this->databaseAdminApi, 'testIamPermissions'], [ + return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ $args['resource'], $args['permissions'], $args diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index e1c7d02a3392..c5f27873f859 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -21,9 +21,9 @@ use Google\Cloud\GrpcRequestWrapper; use Google\Cloud\GrpcTrait; use Google\Cloud\PhpArray; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminApi; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminApi; -use Google\Cloud\Spanner\V1\SpannerApi; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; +use Google\Cloud\Spanner\V1\SpannerClient; use Google\GAX\ApiException; use google\protobuf; use google\spanner\admin\instance\v1\State; @@ -36,19 +36,19 @@ class Grpc implements ConnectionInterface use GrpcTrait; /** - * @var InstanceAdminApi + * @var InstanceAdminClient */ - private $instanceAdminApi; + private $instanceAdminClient; /** - * @var DatabaseAdminApi + * @var DatabaseAdmin */ - private $databaseAdminApi; + private $databaseAdminClient; /** - * @var SpannerApi + * @var SpannerClient */ - private $spannerApi; + private $spannerClient; /** * @var CodecInterface @@ -65,11 +65,6 @@ class Grpc implements ConnectionInterface 'replace' => 'replace', ]; - /** - * @param array $config - */ - public function __construct(array $config) - /** * @param array $config [optional] */ @@ -88,9 +83,9 @@ public function __construct(array $config = []) $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); - $this->instanceAdminApi = new InstanceAdminApi($grpcConfig); - $this->databaseAdminApi = new DatabaseAdminApi($grpcConfig); - $this->spannerApi = new SpannerApi($grpcConfig); + $this->instanceAdminClient = new InstanceAdminClient($grpcConfig); + $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); + $this->spannerClient = new SpannerClient($grpcConfig); } /** @@ -98,7 +93,7 @@ public function __construct(array $config = []) */ public function listConfigs(array $args = []) { - return $this->send([$this->instanceAdminApi, 'listInstanceConfigs'], [ + return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ $args['projectId'], $args ]); @@ -109,7 +104,7 @@ public function listConfigs(array $args = []) */ public function getConfig(array $args = []) { - return $this->send([$this->instanceAdminApi, 'getInstanceConfig'], [ + return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ $args['name'], $args ]); @@ -120,8 +115,8 @@ public function getConfig(array $args = []) */ public function listInstances(array $args = []) { - return $this->send([$this->instanceAdminApi, 'listInstances'], [ - InstanceAdminApi::formatProjectName($args['projectId']), + return $this->send([$this->instanceAdminClient, 'listInstances'], [ + InstanceAdminClient::formatProjectName($args['projectId']), $args ]); } @@ -131,7 +126,7 @@ public function listInstances(array $args = []) */ public function getInstance(array $args = []) { - return $this->send([$this->instanceAdminApi, 'getInstance'], [ + return $this->send([$this->instanceAdminClient, 'getInstance'], [ $args['name'], $args ]); @@ -142,7 +137,7 @@ public function getInstance(array $args = []) */ public function createInstance(array $args = []) { - return $this->send([$this->instanceAdminApi, 'createInstance'], [ + return $this->send([$this->instanceAdminClient, 'createInstance'], [ $args['name'], $args['config'], $args['displayName'], @@ -156,7 +151,7 @@ public function createInstance(array $args = []) */ public function updateInstance(array $args = []) { - return $this->send([$this->instanceAdminApi, 'updateInstance'], [ + return $this->send([$this->instanceAdminClient, 'updateInstance'], [ $args['name'], $args['config'], $args['displayName'], @@ -172,7 +167,7 @@ public function updateInstance(array $args = []) */ public function deleteInstance(array $args = []) { - return $this->send([$this->instanceAdminApi, 'deleteInstance'], [ + return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ $args['name'], $args ]); @@ -183,7 +178,7 @@ public function deleteInstance(array $args = []) */ public function getInstanceIamPolicy(array $args = []) { - return $this->send([$this->instanceAdminApi, 'getIamPolicy'], [ + return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ $args['resource'], $args ]); @@ -194,7 +189,7 @@ public function getInstanceIamPolicy(array $args = []) */ public function setInstanceIamPolicy(array $args = []) { - return $this->send([$this->instanceAdminApi, 'setIamPolicy'], [ + return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ $args['resource'], $args['policy'], $args @@ -206,7 +201,7 @@ public function setInstanceIamPolicy(array $args = []) */ public function testInstanceIamPermissions(array $args = []) { - return $this->send([$this->instanceAdminApi, 'testIamPermissions'], [ + return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ $args['resource'], $args['permissions'], $args @@ -218,7 +213,7 @@ public function testInstanceIamPermissions(array $args = []) */ public function listDatabases(array $args = []) { - return $this->send([$this->databaseAdminApi, 'listDatabases'], [ + return $this->send([$this->databaseAdminClient, 'listDatabases'], [ $args['instance'], $args ]); @@ -229,7 +224,7 @@ public function listDatabases(array $args = []) */ public function createDatabase(array $args = []) { - return $this->send([$this->databaseAdminApi, 'createDatabase'], [ + return $this->send([$this->databaseAdminClient, 'createDatabase'], [ $args['instance'], $args['createStatement'], $args['extraStatements'], @@ -242,7 +237,7 @@ public function createDatabase(array $args = []) */ public function updateDatabase(array $args = []) { - return $this->send([$this->databaseAdminApi, 'updateDatabase'], [ + return $this->send([$this->databaseAdminClient, 'updateDatabase'], [ $args['name'], $args['statements'], $args @@ -254,7 +249,7 @@ public function updateDatabase(array $args = []) */ public function dropDatabase(array $args = []) { - return $this->send([$this->databaseAdminApi, 'dropDatabase'], [ + return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ $args['name'], $args ]); @@ -265,7 +260,7 @@ public function dropDatabase(array $args = []) */ public function getDatabaseDDL(array $args = []) { - return $this->send([$this->databaseAdminApi, 'getDatabaseDDL'], [ + return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ $args['name'], $args ]); @@ -276,7 +271,7 @@ public function getDatabaseDDL(array $args = []) */ public function getDatabaseIamPolicy(array $args = []) { - return $this->send([$this->databaseAdminApi, 'getIamPolicy'], [ + return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ $args['resource'], $args ]); @@ -287,7 +282,7 @@ public function getDatabaseIamPolicy(array $args = []) */ public function setDatabaseIamPolicy(array $args = []) { - return $this->send([$this->databaseAdminApi, 'setIamPolicy'], [ + return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ $args['resource'], $args['policy'], $args @@ -299,7 +294,7 @@ public function setDatabaseIamPolicy(array $args = []) */ public function testDatabaseIamPermissions(array $args = []) { - return $this->send([$this->databaseAdminApi, 'testIamPermissions'], [ + return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ $args['resource'], $args['permissions'], $args @@ -311,7 +306,7 @@ public function testDatabaseIamPermissions(array $args = []) */ public function createSession(array $args = []) { - return $this->send([$this->spannerApi, 'createSession'], [ + return $this->send([$this->spannerClient, 'createSession'], [ $this->pluck('database', $args), $args ]); @@ -322,7 +317,7 @@ public function createSession(array $args = []) */ public function getSession(array $args = []) { - return $this->send([$this->spannerApi, 'getSession'], [ + return $this->send([$this->spannerClient, 'getSession'], [ $this->pluck('name', $args), $args ]); @@ -333,7 +328,7 @@ public function getSession(array $args = []) */ public function deleteSession(array $args = []) { - return $this->send([$this->spannerApi, 'deleteSession'], [ + return $this->send([$this->spannerClient, 'deleteSession'], [ $this->pluck('name', $args), $args ]); @@ -347,7 +342,7 @@ public function executeSql(array $args = []) $args['params'] = (new protobuf\Struct) ->deserialize($this->formatStructForApi($args['params']), $this->codec); - return $this->send([$this->spannerApi, 'executeSql'], [ + return $this->send([$this->spannerClient, 'executeSql'], [ $this->pluck('session', $args), $this->pluck('sql', $args), $args @@ -390,7 +385,7 @@ public function read(array $args = []) $keySet->setAll($keys['all']); } - return $this->send([$this->spannerApi, 'read'], [ + return $this->send([$this->spannerClient, 'read'], [ $this->pluck('session', $args), $this->pluck('table', $args), $this->pluck('columns', $args), @@ -416,7 +411,7 @@ public function beginTransaction(array $args = []) $options->setReadWrite($readWrite); } - return $this->send([$this->spannerApi, 'beginTransaction'], [ + return $this->send([$this->spannerClient, 'beginTransaction'], [ $this->pluck('session', $args), $options, $args @@ -469,7 +464,7 @@ public function commit(array $args = []) $args['singleUseTransaction'] = $options; } - return $this->send([$this->spannerApi, 'commit'], [ + return $this->send([$this->spannerClient, 'commit'], [ $this->pluck('session', $args), $mutations, $args @@ -481,7 +476,7 @@ public function commit(array $args = []) */ public function rollback(array $args = []) { - return $this->send([$this->spannerApi, 'rollback'], [ + return $this->send([$this->spannerClient, 'rollback'], [ $this->pluck('session', $args), $this->pluck('transactionId', $args), $args diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 73c0eaaf786e..403636dd4eae 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -19,7 +19,7 @@ use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminApi; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Google\Cloud\Spanner\Session\SessionPoolInterface; @@ -524,7 +524,7 @@ private function selectSession($context = SessionPoolInterface::CONTEXT_READ) { */ private function fullyQualifiedDatabaseName() { - return DatabaseAdminApi::formatDatabaseName( + return DatabaseAdminClient::formatDatabaseName( $this->projectId, $this->instance->name(), $this->name diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index d03ec9aa6332..5f9f22dfddc8 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -19,8 +19,8 @@ use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminApi; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminApi; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamInstance; use Google\Cloud\Spanner\Session\SessionPoolInterface; @@ -246,7 +246,7 @@ public function update(array $options = []) ); } - $config = InstanceAdminApi::formatInstanceConfigName( + $config = InstanceAdminClient::formatInstanceConfigName( $this->projectId, $options['config']->name() ); @@ -358,7 +358,7 @@ public function databases(array $options = []) if (isset($res['databases'])) { foreach ($res['databases'] as $database) { yield $this->database( - DatabaseAdminApi::parseDatabaseFromDatabaseName($database['name']) + DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']) ); } } @@ -386,7 +386,7 @@ public function iam() */ private function fullyQualifiedInstanceName() { - return InstanceAdminApi::formatInstanceName($this->projectId, $this->name); + return InstanceAdminClient::formatInstanceName($this->projectId, $this->name); } /** diff --git a/src/Spanner/Session/Session.php b/src/Spanner/Session/Session.php index 62790689304b..b0b811b3765a 100644 --- a/src/Spanner/Session/Session.php +++ b/src/Spanner/Session/Session.php @@ -19,7 +19,7 @@ use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Spanner\Connection\ConnectionInterface; -use Google\Cloud\Spanner\V1\SpannerApi; +use Google\Cloud\Spanner\V1\SpannerClient; /** * Represents and manages a single Cloud Spanner session. @@ -149,7 +149,7 @@ public function delete(array $options = []) */ public function name() { - return SpannerApi::formatSessionName( + return SpannerClient::formatSessionName( $this->projectId, $this->instance, $this->database, diff --git a/src/Spanner/Session/SessionClient.php b/src/Spanner/Session/SessionClient.php index 91501a5435d7..3cb21ff3b22d 100644 --- a/src/Spanner/Session/SessionClient.php +++ b/src/Spanner/Session/SessionClient.php @@ -18,7 +18,7 @@ namespace Google\Cloud\Spanner\Session; use Google\Cloud\Spanner\Connection\ConnectionInterface; -use Google\Cloud\Spanner\V1\SpannerApi; +use Google\Cloud\Spanner\V1\SpannerClient; /** * Manage API interactions related to Spanner database sessions. @@ -77,7 +77,7 @@ public function __construct(ConnectionInterface $connection, $projectId) public function create($instance, $database, array $options = []) { $res = $this->connection->createSession($options + [ - 'database' => SpannerApi::formatDatabaseName($this->projectId, $instance, $database) + 'database' => SpannerClient::formatDatabaseName($this->projectId, $instance, $database) ]); $session = null; @@ -85,9 +85,9 @@ public function create($instance, $database, array $options = []) $session = new Session( $this->connection, $this->projectId, - SpannerApi::parseInstanceFromSessionName($res['name']), - SpannerApi::parseDatabaseFromSessionName($res['name']), - SpannerApi::parseSessionFromSessionName($res['name']) + SpannerClient::parseInstanceFromSessionName($res['name']), + SpannerClient::parseDatabaseFromSessionName($res['name']), + SpannerClient::parseSessionFromSessionName($res['name']) ); } diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index c1cf2e6633af..582cd0eb7359 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -19,8 +19,7 @@ use Google\Cloud\ClientTrait; use Google\Cloud\Exception\NotFoundException; -use Google\Cloud\Spanner\Admin\AdminClient; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminApi; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\Grpc; use Google\Cloud\Spanner\Session\SessionClient; use Google\Cloud\Spanner\Session\SimpleSessionPool; @@ -108,12 +107,12 @@ public function __construct(array $config = []) public function configurations() { $res = $this->connection->listConfigs([ - 'projectId' => InstanceAdminApi::formatProjectName($this->projectId) + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId) ]); if (isset($res['instanceConfigs'])) { foreach ($res['instanceConfigs'] as $config) { - $name = InstanceAdminApi::parseInstanceConfigFromInstanceConfigName($config['name']); + $name = InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config['name']); yield $this->configuration($name, $config); } } @@ -170,8 +169,8 @@ public function createInstance(Configuration $config, $name, array $options = [] ]; $res = $this->connection->createInstance($options + [ - 'name' => InstanceAdminApi::formatInstanceName($this->projectId, $name), - 'config' => InstanceAdminApi::formatInstanceConfigName($this->projectId, $config->name()) + 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), + 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) ]); return $this->instance($name); @@ -250,7 +249,7 @@ public function instances(array $options = []) if (isset($res['instances'])) { foreach ($res['instances'] as $instance) { yield $this->instance( - InstanceAdminApi::parseInstanceFromInstanceName($instance['name']), + InstanceAdminClient::parseInstanceFromInstanceName($instance['name']), $instance ); } diff --git a/tests/Spanner/ConfigurationTest.php b/tests/Spanner/ConfigurationTest.php index 29f6effb8881..477e36fb70e2 100644 --- a/tests/Spanner/ConfigurationTest.php +++ b/tests/Spanner/ConfigurationTest.php @@ -18,7 +18,7 @@ namespace Google\Cloud\Tests\Spanner; use Google\Cloud\Exception\NotFoundException; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminApi; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Configuration; use Google\Cloud\Spanner\Connection\AdminConnectionInterface; use Prophecy\Argument; @@ -70,7 +70,7 @@ public function testInfoWithReload() $info = ['foo' => 'bar']; $this->adminConnection->getConfig([ - 'name' => InstanceAdminApi::formatInstanceConfigName(self::PROJECT_ID, self::NAME), + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), 'projectId' => self::PROJECT_ID ])->shouldBeCalled()->willReturn($info); @@ -100,7 +100,7 @@ public function testReload() $info = ['foo' => 'bar']; $this->adminConnection->getConfig([ - 'name' => InstanceAdminApi::formatInstanceConfigName(self::PROJECT_ID, self::NAME), + 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), 'projectId' => self::PROJECT_ID ])->shouldBeCalledTimes(1)->willReturn($info); diff --git a/tests/Spanner/DatabaseTest.php b/tests/Spanner/DatabaseTest.php index ddf34ba8a5ec..5980cccffecb 100644 --- a/tests/Spanner/DatabaseTest.php +++ b/tests/Spanner/DatabaseTest.php @@ -19,7 +19,7 @@ use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminApi; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Connection\AdminConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; @@ -83,7 +83,7 @@ public function testUpdate() { $statements = ['foo', 'bar']; $this->adminConnection->updateDatabase([ - 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), 'statements' => $statements ]); @@ -96,7 +96,7 @@ public function testUpdateWithSingleStatement() { $statement = 'foo'; $this->adminConnection->updateDatabase([ - 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), 'statements' => ['foo'], 'operationId' => null, ])->shouldBeCalled(); @@ -109,7 +109,7 @@ public function testUpdateWithSingleStatement() public function testDrop() { $this->adminConnection->dropDatabase([ - 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->shouldBeCalled(); $this->database->setAdminConnection($this->adminConnection->reveal()); @@ -121,7 +121,7 @@ public function testDdl() { $ddl = ['create table users', 'create table posts']; $this->adminConnection->getDatabaseDDL([ - 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->willReturn(['statements' => $ddl]); $this->database->setAdminConnection($this->adminConnection->reveal()); @@ -132,7 +132,7 @@ public function testDdl() public function testDdlNoResult() { $this->adminConnection->getDatabaseDDL([ - 'name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->willReturn([]); $this->database->setAdminConnection($this->adminConnection->reveal()); diff --git a/tests/Spanner/InstanceTest.php b/tests/Spanner/InstanceTest.php index 62bef6343878..9f8d09a9c327 100644 --- a/tests/Spanner/InstanceTest.php +++ b/tests/Spanner/InstanceTest.php @@ -19,8 +19,8 @@ use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminApi; -use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminApi; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Configuration; use Google\Cloud\Spanner\Connection\AdminConnectionInterface; use Google\Cloud\Spanner\Database; @@ -200,7 +200,7 @@ public function testUpdateWithChanges() 'displayName' => $changes['displayName'], 'nodeCount' => $changes['nodeCount'], 'labels' => $changes['labels'], - 'config' => InstanceAdminApi::formatInstanceConfigName(self::PROJECT_ID, $changes['config']->name()) + 'config' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, $changes['config']->name()) ])->shouldBeCalled(); $this->instance->setAdminConnection($this->adminConnection->reveal()); @@ -231,7 +231,7 @@ public function testUpdateInvalidConfig() public function testDelete() { $this->adminConnection->deleteInstance([ - 'name' => InstanceAdminApi::formatInstanceName(self::PROJECT_ID, self::NAME) + 'name' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME) ])->shouldBeCalled(); $this->instance->setAdminConnection($this->adminConnection->reveal()); @@ -248,7 +248,7 @@ public function testCreateDatabase() $extra = ['foo', 'bar']; $this->adminConnection->createDatabase([ - 'instance' => InstanceAdminApi::formatInstanceName(self::PROJECT_ID, self::NAME), + 'instance' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME), 'createStatement' => 'CREATE DATABASE `test-database`', 'extraStatements' => $extra ]) @@ -275,8 +275,8 @@ public function testDatabase() public function testDatabases() { $databases = [ - ['name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')], - ['name' => DatabaseAdminApi::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')], + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] ]; $this->adminConnection->listDatabases(Argument::any()) From 2ff6c5643be62c9b77c8b04deda4f7bfd5a5527e Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 16 Dec 2016 11:11:52 -0500 Subject: [PATCH 007/107] Fix createInstance request --- .../Connection/AdminConnectionInterface.php | 111 -------- src/Spanner/Connection/AdminGrpc.php | 263 ------------------ src/Spanner/Connection/Grpc.php | 17 +- src/Spanner/SpannerClient.php | 2 + 4 files changed, 15 insertions(+), 378 deletions(-) delete mode 100644 src/Spanner/Connection/AdminConnectionInterface.php delete mode 100644 src/Spanner/Connection/AdminGrpc.php diff --git a/src/Spanner/Connection/AdminConnectionInterface.php b/src/Spanner/Connection/AdminConnectionInterface.php deleted file mode 100644 index 8039c3244039..000000000000 --- a/src/Spanner/Connection/AdminConnectionInterface.php +++ /dev/null @@ -1,111 +0,0 @@ - CredentialsLoader::makeCredentials($config['scopes'], $config['keyFile']) - ]; - - $this->wrapper = new GrpcRequestWrapper; - - $this->instanceAdminClient = new InstanceAdminClient($grpcConfig); - $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); - } - - /** - * @param array $args [optional] - */ - public function listConfigs(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ - $args['projectId'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getConfig(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function listInstances(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'listInstances'], [ - InstanceAdminClient::formatProjectName($args['projectId']), - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getInstance(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'getInstance'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function createInstance(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'createInstance'], [ - $args['name'], - $args['config'], - $args['displayName'], - $args['nodeCount'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function updateInstance(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'updateInstance'], [ - $args['name'], - $args['config'], - $args['displayName'], - $args['nodeCount'], - new State, - $args['labels'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function deleteInstance(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getInstanceIamPolicy(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ - $args['resource'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function setInstanceIamPolicy(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ - $args['resource'], - $args['policy'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function testInstanceIamPermissions(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ - $args['resource'], - $args['permissions'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function listDatabases(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'listDatabases'], [ - $args['instance'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function createDatabase(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'createDatabase'], [ - $args['instance'], - $args['createStatement'], - $args['extraStatements'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function updateDatabase(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'updateDatabase'], [ - $args['name'], - $args['statements'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function dropDatabase(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getDatabaseDDL(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getDatabaseIamPolicy(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ - $args['resource'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function setDatabaseIamPolicy(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ - $args['resource'], - $args['policy'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function testDatabaseIamPermissions(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ - $args['resource'], - $args['permissions'], - $args - ]); - } -} diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index c5f27873f859..3399f4ed1fe3 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -26,6 +26,7 @@ use Google\Cloud\Spanner\V1\SpannerClient; use Google\GAX\ApiException; use google\protobuf; +use google\spanner\admin\instance\v1\Instance; use google\spanner\admin\instance\v1\State; use google\spanner\v1; use google\spanner\v1\Mutation; @@ -137,11 +138,19 @@ public function getInstance(array $args = []) */ public function createInstance(array $args = []) { + $pbInstance = (new Instance())->deserialize(array_filter([ + 'name' => $args['name'], + 'config' => $args['config'], + 'displayName' => $args['displayName'], + 'nodeCount' => $args['nodeCount'], + 'state' => $args['state'], + 'labels' => $this->formatLabelsForApi($args['labels']) + ]), $this->codec); + return $this->send([$this->instanceAdminClient, 'createInstance'], [ - $args['name'], - $args['config'], - $args['displayName'], - $args['nodeCount'], + $args['projectId'], + $args['instanceId'], + $pbInstance, $args ]); } diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 582cd0eb7359..2b3a2196f732 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -169,7 +169,9 @@ public function createInstance(Configuration $config, $name, array $options = [] ]; $res = $this->connection->createInstance($options + [ + 'instanceId' => $name, 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) ]); From 23e1954bc178f1c65c2b286f51471f1ee78fe857 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 16 Dec 2016 12:23:54 -0500 Subject: [PATCH 008/107] Fix Update DDL method --- src/Spanner/Connection/Grpc.php | 2 +- src/Spanner/Instance.php | 3 +- src/Spanner/SpannerClient.php | 50 +++++++++++++++++++++------------ 3 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 3399f4ed1fe3..b1cc9e0caa89 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -246,7 +246,7 @@ public function createDatabase(array $args = []) */ public function updateDatabase(array $args = []) { - return $this->send([$this->databaseAdminClient, 'updateDatabase'], [ + return $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [ $args['name'], $args['statements'], $args diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 5f9f22dfddc8..d685f5484bc4 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -221,7 +221,8 @@ public function state(array $options = []) * @param array $options { * Configuration options * - * @type Configuration $config The configuration to move the instante to. + * @type Configuration $config The configuration to move the instance to. + * } * @return void * @throws \InvalidArgumentException */ diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 2b3a2196f732..e0db7330f457 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -91,35 +91,48 @@ public function __construct(array $config = []) } /** - * List all available configurations + * List all available configurations. * * Example: * ``` * $configurations = $spanner->configurations(); * ``` * - * @todo implement pagination! - * * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs/list List Configs * + * @param array $options [optional] Configuration Options. * @return Generator */ - public function configurations() + public function configurations(array $options = []) { - $res = $this->connection->listConfigs([ - 'projectId' => InstanceAdminClient::formatProjectName($this->projectId) - ]); + $pageToken = null; + do { + $res = $this->connection->listConfigs([ + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), + 'pageToken' => $pageToken + ] + $options); - if (isset($res['instanceConfigs'])) { - foreach ($res['instanceConfigs'] as $config) { - $name = InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config['name']); - yield $this->configuration($name, $config); + if (isset($res['instanceConfigs'])) { + foreach ($res['instanceConfigs'] as $config) { + $name = InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config['name']); + yield $this->configuration($name, $config); + } } - } + + if (isset($res['nextPageToken'])) { + $pageToken = $res['nextPageToken']; + } + } while($pageToken); } /** - * Get a configuration by its name + * Get a configuration by its name. + * + * NOTE: This method does not execute a service request and does not verify + * the existence of the given configuration. Unless you know with certainty + * that the configuration exists, it is advised that you use + * {@see Google\Cloud\Spanner\Configuration::exists()} to verify existence + * before attempting to use the configuration. * * Example: * ``` @@ -136,7 +149,7 @@ public function configuration($name, array $config = []) } /** - * Create an instance + * Create a new instance. * * Example: * ``` @@ -154,7 +167,8 @@ public function configuration($name, array $config = []) * @type string $displayName **Defaults to** the value of $name. * @type int $nodeCount **Defaults to** `1`. * @type int $state **Defaults to** - * @type array $labels [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). + * @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 Instance * @codingStandardsIgnoreEnd @@ -168,18 +182,18 @@ public function createInstance(Configuration $config, $name, array $options = [] 'labels' => [] ]; - $res = $this->connection->createInstance($options + [ + $res = $this->connection->createInstance([ 'instanceId' => $name, 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) - ]); + ] + $options); return $this->instance($name); } /** - * Lazily instantiate an instance + * Lazily instantiate an instance. * * Example: * ``` From 13399b290d708d3b31d285c3510a3c0440856fdb Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 16 Dec 2016 13:49:59 -0500 Subject: [PATCH 009/107] Make Result implement IteratorAggregate --- src/Spanner/Result.php | 52 ++++-------------------------------------- 1 file changed, 5 insertions(+), 47 deletions(-) diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index 35cedd220956..8cab8b645b7f 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -20,7 +20,7 @@ /** * @todo should this be more like BigQuery\QueryResults? */ -class Result implements \Iterator +class Result implements \IteratorAggregate { /** * @var array @@ -32,11 +32,6 @@ class Result implements \Iterator */ private $rows; - /** - * @var int - */ - private $index = 0; - /** * @var array $result The query or read result. */ @@ -57,18 +52,13 @@ public function metadata() } /** - * Return the rows as represented by the API. - * - * For a more easily consumed result in which each row is represented as a - * set of key/value pairs, see {@see Google\Cloud\Spanner\Result::result()}. + * Return the rows as a key/value list. * * @return array|null */ public function rows() { - return (isset($this->result['rows'])) - ? $result['rows'] - : null; + return $this->rows; } /** @@ -127,40 +117,8 @@ private function transformQueryResult(array $result) /** * @access private */ - public function rewind() - { - $this->index = 0; - } - - /** - * @access private - */ - public function current() - { - return $this->rows[$this->index]; - } - - /** - * @access private - */ - public function key() - { - return $this->index; - } - - /** - * @access private - */ - public function next() - { - ++$this->index; - } - - /** - * @access private - */ - public function valid() + public function getIterator() { - return isset($this->rows[$this->index]); + return new \ArrayIterator($this->rows); } } From a7c03690d9cf7a79d959aadca838825a68f69016 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 16 Dec 2016 16:17:31 -0500 Subject: [PATCH 010/107] Fix admin unit tests --- composer.json | 3 +- dev/src/Functions.php | 17 ++++ .../Spanner/Admin}/ConfigurationTest.php | 48 +++++----- .../Admin}/Connection/IamDatabaseTest.php | 16 +--- .../Admin}/Connection/IamInstanceTest.php | 16 +--- .../Spanner/Admin}/DatabaseTest.php | 56 ++++++------ .../Spanner/Admin}/InstanceTest.php | 88 +++++++++---------- .../Spanner/Admin}/SpannerClientTest.php | 34 ++----- 8 files changed, 123 insertions(+), 155 deletions(-) create mode 100644 dev/src/Functions.php rename tests/{Spanner => unit/Spanner/Admin}/ConfigurationTest.php (63%) rename tests/{Spanner => unit/Spanner/Admin}/Connection/IamDatabaseTest.php (83%) rename tests/{Spanner => unit/Spanner/Admin}/Connection/IamInstanceTest.php (83%) rename tests/{Spanner => unit/Spanner/Admin}/DatabaseTest.php (69%) rename tests/{Spanner => unit/Spanner/Admin}/InstanceTest.php (71%) rename tests/{Spanner => unit/Spanner/Admin}/SpannerClientTest.php (78%) diff --git a/composer.json b/composer.json index 4e461b5e3c4e..5d3a4305e8c9 100644 --- a/composer.json +++ b/composer.json @@ -71,7 +71,8 @@ "psr-4": { "Google\\Cloud\\Dev\\": "dev/src", "Google\\Cloud\\Tests\\System\\": "tests/system" - } + }, + "files": ["dev/src/Functions.php"] }, "scripts": { "google-cloud": "dev/google-cloud" diff --git a/dev/src/Functions.php b/dev/src/Functions.php new file mode 100644 index 000000000000..f8a36819cc90 --- /dev/null +++ b/dev/src/Functions.php @@ -0,0 +1,17 @@ +newInstanceArgs($args); +} diff --git a/tests/Spanner/ConfigurationTest.php b/tests/unit/Spanner/Admin/ConfigurationTest.php similarity index 63% rename from tests/Spanner/ConfigurationTest.php rename to tests/unit/Spanner/Admin/ConfigurationTest.php index 477e36fb70e2..b6e388e7548b 100644 --- a/tests/Spanner/ConfigurationTest.php +++ b/tests/unit/Spanner/Admin/ConfigurationTest.php @@ -15,12 +15,12 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner\Admin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Configuration; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Prophecy\Argument; /** @@ -31,17 +31,17 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase const PROJECT_ID = 'test-project'; const NAME = 'test-config'; - private $adminConnection; + private $connection; private $configuration; public function setUp() { - $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); - $this->configuration = new ConfigurationStub( - $this->adminConnection->reveal(), + $this->connection = $this->prophesize(ConnectionInterface::class); + $this->configuration = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), self::PROJECT_ID, self::NAME - ); + ]); } public function testName() @@ -51,16 +51,16 @@ public function testName() public function testInfo() { - $this->adminConnection->getConfig(Argument::any())->shouldNotBeCalled(); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->connection->getConfig(Argument::any())->shouldNotBeCalled(); + $this->configuration->setConnection($this->connection->reveal()); $info = ['foo' => 'bar']; - $config = new ConfigurationStub( - $this->adminConnection->reveal(), + $config = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), self::PROJECT_ID, self::NAME, $info - ); + ]); $this->assertEquals($info, $config->info()); } @@ -69,28 +69,28 @@ public function testInfoWithReload() { $info = ['foo' => 'bar']; - $this->adminConnection->getConfig([ + $this->connection->getConfig([ 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), 'projectId' => self::PROJECT_ID ])->shouldBeCalled()->willReturn($info); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->configuration->setConnection($this->connection->reveal()); $this->assertEquals($info, $this->configuration->info()); } public function testExists() { - $this->adminConnection->getConfig(Argument::any())->willReturn([]); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->connection->getConfig(Argument::any())->willReturn([]); + $this->configuration->setConnection($this->connection->reveal()); $this->assertTrue($this->configuration->exists()); } public function testExistsDoesntExist() { - $this->adminConnection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404)); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->connection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404)); + $this->configuration->setConnection($this->connection->reveal()); $this->assertFalse($this->configuration->exists()); } @@ -99,12 +99,12 @@ public function testReload() { $info = ['foo' => 'bar']; - $this->adminConnection->getConfig([ + $this->connection->getConfig([ 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), 'projectId' => self::PROJECT_ID ])->shouldBeCalledTimes(1)->willReturn($info); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->configuration->setConnection($this->connection->reveal()); $info = $this->configuration->reload(); @@ -113,11 +113,3 @@ public function testReload() $this->assertEquals($info, $info2); } } - -class ConfigurationStub extends Configuration -{ - public function setAdminConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/Spanner/Connection/IamDatabaseTest.php b/tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php similarity index 83% rename from tests/Spanner/Connection/IamDatabaseTest.php rename to tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php index 870646654481..10d518dcfa3e 100644 --- a/tests/Spanner/Connection/IamDatabaseTest.php +++ b/tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php @@ -15,9 +15,9 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner\Connection; +namespace Google\Cloud\Tests\Unit\Spanner\Admin\Connection; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Prophecy\Argument; @@ -32,9 +32,9 @@ class IamDatabaseTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->connection = $this->prophesize(AdminConnectionInterface::class); + $this->connection = $this->prophesize(ConnectionInterface::class); - $this->iam = new IamDatabaseStub($this->connection->reveal()); + $this->iam = \Google\Cloud\Dev\stub(IamDatabase::class, [$this->connection->reveal()]); } public function testGetPolicy() @@ -85,11 +85,3 @@ public function testTestPermissions() $this->assertEquals($res, $p); } } - -class IamDatabaseStub extends IamDatabase -{ - public function setConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/Spanner/Connection/IamInstanceTest.php b/tests/unit/Spanner/Admin/Connection/IamInstanceTest.php similarity index 83% rename from tests/Spanner/Connection/IamInstanceTest.php rename to tests/unit/Spanner/Admin/Connection/IamInstanceTest.php index 77cb7d12ed4c..a979ed863df9 100644 --- a/tests/Spanner/Connection/IamInstanceTest.php +++ b/tests/unit/Spanner/Admin/Connection/IamInstanceTest.php @@ -15,9 +15,9 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner\Connection; +namespace Google\Cloud\Tests\Unit\Spanner\Admin\Connection; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamInstance; use Prophecy\Argument; @@ -32,9 +32,9 @@ class IamInstanceTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->connection = $this->prophesize(AdminConnectionInterface::class); + $this->connection = $this->prophesize(ConnectionInterface::class); - $this->iam = new IamInstanceStub($this->connection->reveal()); + $this->iam = \Google\Cloud\Dev\stub(IamInstance::class, [$this->connection->reveal()]); } public function testGetPolicy() @@ -85,11 +85,3 @@ public function testTestPermissions() $this->assertEquals($res, $p); } } - -class IamInstanceStub extends IamInstance -{ - public function setConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/Spanner/DatabaseTest.php b/tests/unit/Spanner/Admin/DatabaseTest.php similarity index 69% rename from tests/Spanner/DatabaseTest.php rename to tests/unit/Spanner/Admin/DatabaseTest.php index 5980cccffecb..a65982745ff2 100644 --- a/tests/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/Admin/DatabaseTest.php @@ -15,14 +15,15 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner\Admin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Session\SessionPoolInterface; use Prophecy\Argument; /** @@ -34,22 +35,23 @@ class DatabaseTest extends \PHPUnit_Framework_TestCase const INSTANCE_NAME = 'test-instance'; const NAME = 'test-database'; - private $adminConnection; + private $connection; private $instance; private $database; public function setUp() { - $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); + $this->connection = $this->prophesize(ConnectionInterface::class); $this->instance = $this->prophesize(Instance::class); $this->instance->name()->willReturn(self::INSTANCE_NAME); - $this->database = new DatabaseStub( - $this->adminConnection->reveal(), + $this->database = \Google\Cloud\Dev\stub(Database::class, [ + $this->connection->reveal(), $this->instance->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), self::PROJECT_ID, self::NAME - ); + ]); } public function testName() @@ -59,22 +61,22 @@ public function testName() public function testExists() { - $this->adminConnection->getDatabaseDDL(Argument::any()) + $this->connection->getDatabaseDDL(Argument::any()) ->shouldBeCalled() ->willReturn([]); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->setConnection($this->connection->reveal()); $this->assertTrue($this->database->exists()); } public function testExistsNotFound() { - $this->adminConnection->getDatabaseDDL(Argument::any()) + $this->connection->getDatabaseDDL(Argument::any()) ->shouldBeCalled() ->willThrow(new NotFoundException('', 404)); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->setConnection($this->connection->reveal()); $this->assertFalse($this->database->exists()); } @@ -82,37 +84,37 @@ public function testExistsNotFound() public function testUpdate() { $statements = ['foo', 'bar']; - $this->adminConnection->updateDatabase([ + $this->connection->updateDatabase([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), 'statements' => $statements ]); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->setConnection($this->connection->reveal()); - $this->database->update($statements); + $this->database->updateDdl($statements); } public function testUpdateWithSingleStatement() { $statement = 'foo'; - $this->adminConnection->updateDatabase([ + $this->connection->updateDatabase([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), 'statements' => ['foo'], 'operationId' => null, ])->shouldBeCalled(); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->setConnection($this->connection->reveal()); - $this->database->update($statement); + $this->database->updateDdl($statement); } public function testDrop() { - $this->adminConnection->dropDatabase([ + $this->connection->dropDatabase([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->shouldBeCalled(); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->setConnection($this->connection->reveal()); $this->database->drop(); } @@ -120,22 +122,22 @@ public function testDrop() public function testDdl() { $ddl = ['create table users', 'create table posts']; - $this->adminConnection->getDatabaseDDL([ + $this->connection->getDatabaseDDL([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->willReturn(['statements' => $ddl]); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->setConnection($this->connection->reveal()); $this->assertEquals($ddl, $this->database->ddl()); } public function testDdlNoResult() { - $this->adminConnection->getDatabaseDDL([ + $this->connection->getDatabaseDDL([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->willReturn([]); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->setConnection($this->connection->reveal()); $this->assertEquals([], $this->database->ddl()); } @@ -145,11 +147,3 @@ public function testIam() $this->assertInstanceOf(Iam::class, $this->database->iam()); } } - -class DatabaseStub extends Database -{ - public function setAdminConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/Spanner/InstanceTest.php b/tests/unit/Spanner/Admin/InstanceTest.php similarity index 71% rename from tests/Spanner/InstanceTest.php rename to tests/unit/Spanner/Admin/InstanceTest.php index 9f8d09a9c327..85e6b6f0c5c3 100644 --- a/tests/Spanner/InstanceTest.php +++ b/tests/unit/Spanner/Admin/InstanceTest.php @@ -15,16 +15,17 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner\Admin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Configuration; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Session\SessionPoolInterface; use Prophecy\Argument; /** @@ -35,13 +36,18 @@ class InstanceTest extends \PHPUnit_Framework_TestCase const PROJECT_ID = 'test-project'; const NAME = 'instance-name'; - private $adminConnection; + private $connection; private $instance; public function setUp() { - $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); - $this->instance = new InstanceStub($this->adminConnection->reveal(), self::PROJECT_ID, self::NAME); + $this->connection = $this->prophesize(ConnectionInterface::class); + $this->instance = \Google\Cloud\Dev\stub(Instance::class, [ + $this->connection->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), + self::PROJECT_ID, + self::NAME + ]); } public function testName() @@ -51,9 +57,9 @@ public function testName() public function testInfo() { - $this->adminConnection->getInstance()->shouldNotBeCalled(); + $this->connection->getInstance()->shouldNotBeCalled(); - $instance = new Instance($this->adminConnection->reveal(), self::PROJECT_ID, self::NAME, ['foo' => 'bar']); + $instance = new Instance($this->connection->reveal(), $this->prophesize(SessionPoolInterface::class)->reveal(), self::PROJECT_ID, self::NAME, ['foo' => 'bar']); $this->assertEquals('bar', $instance->info()['foo']); } @@ -61,11 +67,11 @@ public function testInfoWithReload() { $instance = $this->getDefaultInstance(); - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $info = $this->instance->info(); $this->assertEquals('Instance Name', $info['displayName']); @@ -75,20 +81,20 @@ public function testInfoWithReload() public function testExists() { - $this->adminConnection->getInstance(Argument::any())->shouldBeCalled()->willReturn([]); + $this->connection->getInstance(Argument::any())->shouldBeCalled()->willReturn([]); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->assertTrue($this->instance->exists()); } public function testExistsNotFound() { - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalled() ->willThrow(new NotFoundException('foo', 404)); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->assertFalse($this->instance->exists()); } @@ -97,11 +103,11 @@ public function testReload() { $instance = $this->getDefaultInstance(); - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $info = $this->instance->reload(); @@ -112,22 +118,22 @@ public function testState() { $instance = $this->getDefaultInstance(); - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->assertEquals(Instance::STATE_READY, $this->instance->state()); } public function testStateIsNull() { - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn([]); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->assertNull($this->instance->state()); } @@ -136,11 +142,11 @@ public function testUpdate() { $instance = $this->getDefaultInstance(); - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->adminConnection->updateInstance([ + $this->connection->updateInstance([ 'name' => $instance['name'], 'displayName' => $instance['displayName'], 'nodeCount' => $instance['nodeCount'], @@ -148,7 +154,7 @@ public function testUpdate() 'config' => $instance['config'] ])->shouldBeCalled(); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->instance->update(); } @@ -158,11 +164,11 @@ public function testUpdateWithExistingLabels() $instance = $this->getDefaultInstance(); $instance['labels'] = ['foo' => 'bar']; - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->adminConnection->updateInstance([ + $this->connection->updateInstance([ 'name' => $instance['name'], 'displayName' => $instance['displayName'], 'nodeCount' => $instance['nodeCount'], @@ -170,7 +176,7 @@ public function testUpdateWithExistingLabels() 'config' => $instance['config'] ])->shouldBeCalled(); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->instance->update(); } @@ -191,11 +197,11 @@ public function testUpdateWithChanges() 'config' => $config->reveal() ]; - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->adminConnection->updateInstance([ + $this->connection->updateInstance([ 'name' => $instance['name'], 'displayName' => $changes['displayName'], 'nodeCount' => $changes['nodeCount'], @@ -203,7 +209,7 @@ public function testUpdateWithChanges() 'config' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, $changes['config']->name()) ])->shouldBeCalled(); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->instance->update($changes); } @@ -219,22 +225,22 @@ public function testUpdateInvalidConfig() 'config' => 'foo' ]; - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->instance->update($changes); } public function testDelete() { - $this->adminConnection->deleteInstance([ + $this->connection->deleteInstance([ 'name' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME) ])->shouldBeCalled(); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $this->instance->delete(); } @@ -247,7 +253,7 @@ public function testCreateDatabase() $extra = ['foo', 'bar']; - $this->adminConnection->createDatabase([ + $this->connection->createDatabase([ 'instance' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME), 'createStatement' => 'CREATE DATABASE `test-database`', 'extraStatements' => $extra @@ -255,7 +261,7 @@ public function testCreateDatabase() ->shouldBeCalled() ->willReturn($dbInfo); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $database = $this->instance->createDatabase('test-database', [ 'statements' => $extra @@ -279,11 +285,11 @@ public function testDatabases() ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] ]; - $this->adminConnection->listDatabases(Argument::any()) + $this->connection->listDatabases(Argument::any()) ->shouldBeCalled() ->willReturn(['databases' => $databases]); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->setConnection($this->connection->reveal()); $dbs = $this->instance->databases(); @@ -305,14 +311,6 @@ public function testIam() private function getDefaultInstance() { - return json_decode(file_get_contents(__DIR__ .'/../fixtures/spanner/instance.json'), true); - } -} - -class InstanceStub extends Instance -{ - public function setAdminConnection($conn) - { - $this->adminConnection = $conn; + return json_decode(file_get_contents(__DIR__ .'/../../../fixtures/spanner/instance.json'), true); } } diff --git a/tests/Spanner/SpannerClientTest.php b/tests/unit/Spanner/Admin/SpannerClientTest.php similarity index 78% rename from tests/Spanner/SpannerClientTest.php rename to tests/unit/Spanner/Admin/SpannerClientTest.php index 0ec7198d7862..295ff6318ace 100644 --- a/tests/Spanner/SpannerClientTest.php +++ b/tests/unit/Spanner/Admin/SpannerClientTest.php @@ -15,11 +15,10 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\Spanner\Admin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Spanner\Configuration; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\SpannerClient; @@ -34,21 +33,17 @@ class SpannerClientTest extends \PHPUnit_Framework_TestCase private $connection; - private $adminConnection; - public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); - $this->client = new SpannerClientStub(['projectId' => 'test-project']); + $this->client = \Google\Cloud\Dev\stub(SpannerClient::class, [['projectId' => 'test-project']]); $this->client->setConnection($this->connection->reveal()); - $this->client->setAdminConnection($this->adminConnection->reveal()); } public function testConfigurations() { - $this->adminConnection->listConfigs(Argument::any()) + $this->connection->listConfigs(Argument::any()) ->shouldBeCalled() ->willReturn([ 'instanceConfigs' => [ @@ -62,7 +57,7 @@ public function testConfigurations() ] ]); - $this->client->setAdminConnection($this->adminConnection->reveal()); + $this->client->setConnection($this->connection->reveal()); $configs = $this->client->configurations(); @@ -84,7 +79,7 @@ public function testConfiguration() public function testCreateInstance() { - $this->adminConnection->createInstance(Argument::that(function ($arg) { + $this->connection->createInstance(Argument::that(function ($arg) { if ($arg['name'] !== 'projects/test-project/instances/foo') return false; if ($arg['config'] !== 'projects/test-project/instanceConfigs/my-config') return false; @@ -93,7 +88,7 @@ public function testCreateInstance() ->shouldBeCalled() ->willReturn([]); - $this->client->setAdminConnection($this->adminConnection->reveal()); + $this->client->setConnection($this->connection->reveal()); $config = $this->prophesize(Configuration::class); $config->name()->willReturn('my-config'); @@ -119,7 +114,7 @@ public function testInstanceWithInstanceArray() public function testInstances() { - $this->adminConnection->listInstances(Argument::any()) + $this->connection->listInstances(Argument::any()) ->shouldBeCalled() ->willReturn([ 'instances' => [ @@ -128,7 +123,7 @@ public function testInstances() ] ]); - $this->client->setAdminConnection($this->adminConnection->reveal()); + $this->client->setConnection($this->connection->reveal()); $instances = $this->client->instances(); $this->assertInstanceOf(\Generator::class, $instances); @@ -139,16 +134,3 @@ public function testInstances() $this->assertEquals('bar', $instances[1]->name()); } } - -class SpannerClientStub extends SpannerClient -{ - public function setConnection($conn) - { - $this->connection = $conn; - } - - public function setAdminConnection($conn) - { - $this->adminConnection = $conn; - } -} From 22f0fda9e1bab71244fd4d3ff2c2a15de7273981 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 16 Dec 2016 16:32:14 -0500 Subject: [PATCH 011/107] Update docs and add updateDdlBatch method --- src/Spanner/Configuration.php | 20 +++++++-- src/Spanner/Database.php | 52 ++++++++++++++++++----- src/Spanner/Instance.php | 5 +++ src/Spanner/SpannerClient.php | 20 +++++++++ tests/unit/Spanner/Admin/DatabaseTest.php | 15 ++++++- 5 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php index 882f729a6180..5ab802dafc83 100644 --- a/src/Spanner/Configuration.php +++ b/src/Spanner/Configuration.php @@ -22,7 +22,7 @@ use Google\Cloud\Spanner\Connection\ConnectionInterface; /** - * Represents a Cloud Spanner Configuration + * Represents a Cloud Spanner Configuration. * * Example: * ``` @@ -33,6 +33,8 @@ * * $configuration = $spanner->configuration('regional-europe-west'); * ``` + * + * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs Instance Configs */ class Configuration { @@ -104,8 +106,10 @@ public function name() * echo $info['nodeCount']; * ``` * + * @codingStandardsIgnoreStart * @param array $options [optional] Configuration options. - * @return array + * @return array [https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs#InstanceConfig](InstanceConfig) + * @codingStandardsIgnoreEnd */ public function info(array $options = []) { @@ -129,7 +133,7 @@ public function info(array $options = []) * ``` * * @param array $options [optional] Configuration options. - * @return array + * @return bool */ public function exists(array $options = []) { @@ -150,8 +154,10 @@ public function exists(array $options = []) * $info = $configuration->reload(); * ``` * + * @codingStandardsIgnoreStart * @param array $options [optional] Configuration options. - * @return array + * @return array [https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs#InstanceConfig](InstanceConfig) + * @codingStandardsIgnoreEnd */ public function reload(array $options = []) { @@ -163,6 +169,12 @@ public function reload(array $options = []) return $this->info; } + /** + * A more readable representation of the object. + * + * @codeCoverageIgnore + * @access private + */ public function __debugInfo() { return [ diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 403636dd4eae..00cc2bd74e80 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -29,7 +29,12 @@ * * Example: * ``` - * $database = $instance->database('my-database'); + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $database = $spanner->connect('my-instance-name', 'my-database'); * ``` */ class Database @@ -78,7 +83,6 @@ class Database * @param SessionPoolInterface The session pool implementation. * @param string $projectId The project ID. * @param string $name The database name. - * @param array $info [optional] A representation of the database object. */ public function __construct( ConnectionInterface $connection, @@ -144,33 +148,61 @@ public function exists(array $options = []) } /** - * Update the Database. + * Update the Database schema by running a SQL statement. * * Example: * ``` - * $database->update([ + * $database->updateDdl( * 'CREATE TABLE Users ( * id INT64 NOT NULL, * name STRING(100) NOT NULL * password STRING(100) NOT NULL * )' - * ]); + * ); * ``` * - * @param string|array $statements One or more DDL statements to execute. + * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language + * + * @param string $statement A DDL statement to run against a database. * @param array $options [optional] Configuration options. * @return */ public function updateDdl($statements, array $options = []) + { + return $this->updateDdlBatch([$statements], $options); + } + + /** + * Update the Database schema by running a set of SQL statements. + * + * Example: + * ``` + * $database->updateDdlBatch([ + * 'CREATE TABLE Users ( + * id INT64 NOT NULL, + * name STRING(100) NOT NULL + * password STRING(100) NOT NULL + * )', + * 'CREATE TABLE Posts ( + * id INT64 NOT NULL, + * title STRING(100) NOT NULL + * content STRING(MAX) NOT NULL + * )' + * ]); + * ``` + * + * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language + * + * @param string[] $statements A list of DDL statements to run against a database. + * @param array $options [optional] Configuration options. + * @return + */ + public function updateDdlBatch(array $statements, array $options = []) { $options += [ 'operationId' => null ]; - if (!is_array($statements)) { - $statements = [$statements]; - } - return $this->connection->updateDatabase($options + [ 'name' => $this->fullyQualifiedDatabaseName(), 'statements' => $statements, diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index d685f5484bc4..57cc25a58788 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -31,6 +31,11 @@ * * Example: * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * * $instance = $spanner->instance('my-instance'); * ``` */ diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index e0db7330f457..3be6db3aeb0d 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -26,6 +26,26 @@ use Google\Cloud\ValidateTrait; use google\spanner\admin\instance\v1\Instance\State; +/** + * Google Cloud Spanner is a highly scalable, transactional, managed, NewSQL + * database service. Find more information at + * [Google Cloud Spanner docs](https://cloud.google.com/spanner/). + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * ``` + * + * ``` + * // SpannerClient can be instantiated directly. + * use Google\Cloud\Spanner\SpannerClient; + * + * $spanner = new SpannerClient(); + * ``` + */ class SpannerClient { use ClientTrait; diff --git a/tests/unit/Spanner/Admin/DatabaseTest.php b/tests/unit/Spanner/Admin/DatabaseTest.php index a65982745ff2..655f26172b92 100644 --- a/tests/unit/Spanner/Admin/DatabaseTest.php +++ b/tests/unit/Spanner/Admin/DatabaseTest.php @@ -81,7 +81,20 @@ public function testExistsNotFound() $this->assertFalse($this->database->exists()); } - public function testUpdate() + public function testUpdateDdl() + { + $statement = 'foo'; + $this->connection->updateDatabase([ + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), + 'statements' => [$statement] + ]); + + $this->database->setConnection($this->connection->reveal()); + + $this->database->updateDdl($statement); + } + + public function testUpdateDdlBatch() { $statements = ['foo', 'bar']; $this->connection->updateDatabase([ From bb47fb5587e30158468affadde397caf52452e8a Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 21 Dec 2016 11:47:33 -0500 Subject: [PATCH 012/107] Support disabling grpc key conversion --- src/PhpArray.php | 15 +++++- src/ServiceBuilder.php | 20 +++++++ src/Spanner/Connection/Grpc.php | 95 +++++++++++++++++---------------- 3 files changed, 83 insertions(+), 47 deletions(-) diff --git a/src/PhpArray.php b/src/PhpArray.php index 984061bbbe3f..f2a4029b368f 100644 --- a/src/PhpArray.php +++ b/src/PhpArray.php @@ -32,13 +32,24 @@ class PhpArray extends Protobuf\Codec\PhpArray */ private $customFilters; + /** + * @var bool + */ + private $useCamelCase; + /** * @param array $customFilters A set of callbacks to apply to properties in * a gRPC response. */ - public function __construct(array $customFilters = []) + public function __construct(array $customFilters = [], array $config = []) { $this->customFilters = $customFilters; + + $config = $config + [ + 'useCamelCase' => true + ]; + + $this->useCamelCase = $config['useCamelCase']; } /** @@ -96,7 +107,7 @@ protected function encodeMessage(Protobuf\Message $message) $v = $this->filterValue($v, $field); } - $key = $this->toCamelCase($key); + $key = ($this->useCamelCase) ? $this->toCamelCase($key) : $key; if (isset($this->customFilters[$key])) { $v = call_user_func($this->customFilters[$key], $v); diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index 61a9d523c59f..a0519c357a0c 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -23,6 +23,7 @@ use Google\Cloud\Logging\LoggingClient; use Google\Cloud\NaturalLanguage\NaturalLanguageClient; use Google\Cloud\PubSub\PubSubClient; +use Google\Cloud\Spanner\SpannerClient; use Google\Cloud\Speech\SpeechClient; use Google\Cloud\Storage\StorageClient; use Google\Cloud\Translate\TranslateClient; @@ -209,6 +210,25 @@ public function pubsub(array $config = []) return new PubSubClient($config ? $this->resolveConfig($config) : $this->config); } + /** + * Google Cloud Spanner client. Google Cloud Spanner is a highly scalable, + * transactional, managed, NewSQL database service. Find more information + * at [Google Cloud Spanner API docs](https://cloud.google.com/spanner/). + * + * Example: + * ``` + * $spanner = $cloud->spanner(); + * ``` + * + * @param array $config [optional] Configuration options. See + * {@see Google\Cloud\ServiceBuilder::__construct()} for the available options. + * @return SpannerClient + */ + public function spanner(array $config = []) + { + return new SpannerClient($config ? $this->resolveConfig($config) : $this->config); + } + /** * Google Cloud Speech client. Enables easy integration of Google speech * recognition technologies into developer applications. Send audio and diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index b1cc9e0caa89..d0be511e6372 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -71,10 +71,6 @@ class Grpc implements ConnectionInterface */ public function __construct(array $config = []) { - $grpcConfig = [ - 'credentialsLoader' => CredentialsLoader::makeCredentials($config['scopes'], $config['keyFile']) - ]; - $this->codec = new PhpArray([ 'timestamp' => function ($v) { return $this->formatTimestampFromApi($v); @@ -84,6 +80,7 @@ public function __construct(array $config = []) $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); + $grpcConfig = $this->getGaxConfig(); $this->instanceAdminClient = new InstanceAdminClient($grpcConfig); $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); $this->spannerClient = new SpannerClient($grpcConfig); @@ -95,7 +92,7 @@ public function __construct(array $config = []) public function listConfigs(array $args = []) { return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ - $args['projectId'], + $this->pluck('projectId', $args), $args ]); } @@ -106,7 +103,7 @@ public function listConfigs(array $args = []) public function getConfig(array $args = []) { return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -117,7 +114,7 @@ public function getConfig(array $args = []) public function listInstances(array $args = []) { return $this->send([$this->instanceAdminClient, 'listInstances'], [ - InstanceAdminClient::formatProjectName($args['projectId']), + $this->pluck('projectId', $args), $args ]); } @@ -128,7 +125,7 @@ public function listInstances(array $args = []) public function getInstance(array $args = []) { return $this->send([$this->instanceAdminClient, 'getInstance'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -138,19 +135,10 @@ public function getInstance(array $args = []) */ public function createInstance(array $args = []) { - $pbInstance = (new Instance())->deserialize(array_filter([ - 'name' => $args['name'], - 'config' => $args['config'], - 'displayName' => $args['displayName'], - 'nodeCount' => $args['nodeCount'], - 'state' => $args['state'], - 'labels' => $this->formatLabelsForApi($args['labels']) - ]), $this->codec); - return $this->send([$this->instanceAdminClient, 'createInstance'], [ - $args['projectId'], - $args['instanceId'], - $pbInstance, + $this->pluck('projectId', $args), + $this->pluck('instanceId', $args), + $this->instanceObject($args, true), $args ]); } @@ -160,24 +148,41 @@ public function createInstance(array $args = []) */ public function updateInstance(array $args = []) { + $instanceObject = $this->instanceObject($args); + + $mask = array_keys($instanceObject->serialize(new PhpArray([], ['useCamelCase' => false]))); + + $fieldMask = new protobuf\FieldMask(); + array_walk($mask, function (&$element) use ($fieldMask) { + $fieldMask->addPaths($element); + }); + return $this->send([$this->instanceAdminClient, 'updateInstance'], [ - $args['name'], - $args['config'], - $args['displayName'], - $args['nodeCount'], - new State, - $args['labels'], + $instanceObject, + $fieldMask, $args ]); } + private function instanceObject(array &$args, $required = false) + { + return (new Instance())->deserialize(array_filter([ + 'name' => $this->pluck('name', $args, $required), + 'config' => $this->pluck('config', $args, $required), + 'displayName' => $this->pluck('displayName', $args, $required), + 'nodeCount' => $this->pluck('nodeCount', $args, $required), + 'state' => $this->pluck('state', $args, $required), + 'labels' => $this->formatLabelsForApi($this->pluck('labels', $args, $required)) + ]), $this->codec); + } + /** * @param array $args [optional] */ public function deleteInstance(array $args = []) { return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -188,7 +193,7 @@ public function deleteInstance(array $args = []) public function getInstanceIamPolicy(array $args = []) { return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ - $args['resource'], + $this->pluck('resource', $args), $args ]); } @@ -199,8 +204,8 @@ public function getInstanceIamPolicy(array $args = []) public function setInstanceIamPolicy(array $args = []) { return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ - $args['resource'], - $args['policy'], + $this->pluck('resource', $args), + $this->pluck('policy', $args), $args ]); } @@ -211,8 +216,8 @@ public function setInstanceIamPolicy(array $args = []) public function testInstanceIamPermissions(array $args = []) { return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ - $args['resource'], - $args['permissions'], + $this->pluck('resource', $args), + $this->pluck('permissions', $args), $args ]); } @@ -223,7 +228,7 @@ public function testInstanceIamPermissions(array $args = []) public function listDatabases(array $args = []) { return $this->send([$this->databaseAdminClient, 'listDatabases'], [ - $args['instance'], + $this->pluck('instance', $args), $args ]); } @@ -234,9 +239,9 @@ public function listDatabases(array $args = []) public function createDatabase(array $args = []) { return $this->send([$this->databaseAdminClient, 'createDatabase'], [ - $args['instance'], - $args['createStatement'], - $args['extraStatements'], + $this->pluck('instance', $args), + $this->pluck('createStatement', $args), + $this->pluck('extraStatements', $args), $args ]); } @@ -247,8 +252,8 @@ public function createDatabase(array $args = []) public function updateDatabase(array $args = []) { return $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [ - $args['name'], - $args['statements'], + $this->pluck('name', $args), + $this->pluck('statements', $args), $args ]); } @@ -259,7 +264,7 @@ public function updateDatabase(array $args = []) public function dropDatabase(array $args = []) { return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -270,7 +275,7 @@ public function dropDatabase(array $args = []) public function getDatabaseDDL(array $args = []) { return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -281,7 +286,7 @@ public function getDatabaseDDL(array $args = []) public function getDatabaseIamPolicy(array $args = []) { return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ - $args['resource'], + $this->pluck('resource', $args), $args ]); } @@ -292,8 +297,8 @@ public function getDatabaseIamPolicy(array $args = []) public function setDatabaseIamPolicy(array $args = []) { return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ - $args['resource'], - $args['policy'], + $this->pluck('resource', $args), + $this->pluck('policy', $args), $args ]); } @@ -304,8 +309,8 @@ public function setDatabaseIamPolicy(array $args = []) public function testDatabaseIamPermissions(array $args = []) { return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ - $args['resource'], - $args['permissions'], + $this->pluck('resource', $args), + $this->pluck('permissions', $args), $args ]); } From 2114edfa69ade245126cd43edb3b7506ba9a96bf Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 21 Dec 2016 11:48:05 -0500 Subject: [PATCH 013/107] Add snippet and unit tests for all but Grpc connection --- src/Spanner/Configuration.php | 4 +- src/Spanner/Database.php | 21 +- src/Spanner/Instance.php | 68 +++--- src/Spanner/Operation.php | 12 +- src/Spanner/SpannerClient.php | 59 ++--- .../Spanner/Admin/ConfigurationTest.php | 119 ++++++++++ tests/snippets/Spanner/Admin/DatabaseTest.php | 147 ++++++++++++ tests/snippets/Spanner/Admin/InstanceTest.php | 215 ++++++++++++++++++ .../Spanner/Admin/SpannerClientTest.php | 129 +++++++++++ .../unit/Spanner/Admin/ConfigurationTest.php | 2 +- .../Admin/Connection/IamDatabaseTest.php | 2 +- .../Admin/Connection/IamInstanceTest.php | 2 +- tests/unit/Spanner/Admin/DatabaseTest.php | 2 +- tests/unit/Spanner/Admin/InstanceTest.php | 29 +-- .../unit/Spanner/Admin/SpannerClientTest.php | 39 +++- 15 files changed, 735 insertions(+), 115 deletions(-) create mode 100644 tests/snippets/Spanner/Admin/ConfigurationTest.php create mode 100644 tests/snippets/Spanner/Admin/DatabaseTest.php create mode 100644 tests/snippets/Spanner/Admin/InstanceTest.php create mode 100644 tests/snippets/Spanner/Admin/SpannerClientTest.php diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php index 5ab802dafc83..6d59c1bd748a 100644 --- a/src/Spanner/Configuration.php +++ b/src/Spanner/Configuration.php @@ -103,7 +103,7 @@ public function name() * Example: * ``` * $info = $configuration->info(); - * echo $info['nodeCount']; + * echo $info['displayName']; * ``` * * @codingStandardsIgnoreStart @@ -128,7 +128,7 @@ public function info(array $options = []) * Example: * ``` * if ($configuration->exists()) { - * echo 'The configuration exists!'; + * echo 'Configuration exists!'; * } * ``` * diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 00cc2bd74e80..7e0498cfd531 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -34,7 +34,18 @@ * $cloud = new ServiceBuilder(); * $spanner = $cloud->spanner(); * - * $database = $spanner->connect('my-instance-name', 'my-database'); + * $database = $spanner->connect('my-instance', 'my-database'); + * ``` + * + * ``` + * // Databases can also be connected to via an Instance. + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $instance = $spanner->instance('my-instance'); + * $database = $instance->database('my-database'); * ``` */ class Database @@ -97,7 +108,7 @@ public function __construct( $this->projectId = $projectId; $this->name = $name; - $this->operation = new Operation($connection, $instance, $this); + $this->operation = new Operation($connection); $this->iam = new Iam( new IamDatabase($this->connection), $this->fullyQualifiedDatabaseName() @@ -127,7 +138,7 @@ public function name() * Example: * ``` * if ($database->exists()) { - * echo 'The database exists!'; + * echo 'Database exists!'; * } * ``` * @@ -137,9 +148,7 @@ public function name() public function exists(array $options = []) { try { - $this->connection->getDatabaseDDL($options + [ - 'name' => $this->fullyQualifiedDatabaseName() - ]); + $this->ddl($options); } catch (NotFoundException $e) { return false; } diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 57cc25a58788..4a741bf498a2 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -148,7 +148,7 @@ public function info(array $options = []) * Example: * ``` * if ($instance->exists()) { - * echo 'The instance exists!'; + * echo 'Instance exists!'; * } * ``` * @@ -195,9 +195,8 @@ public function reload(array $options = []) * * Example: * ``` - * $instance = $spanner->createInstance($config, 'my-new-instance'); * if ($instance->state() === Instance::STATE_READY) { - * // do stuff + * echo 'Instance is ready!'; * } * ``` * @@ -218,15 +217,21 @@ public function state(array $options = []) * * Example: * ``` - * todo + * $instance->update([ + * 'displayName' => 'My Instance', + * 'nodeCount' => 4 + * ]); * ``` * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1 Update Instance * - * @param array $options { + * @param array $options [optional] { * Configuration options * - * @type Configuration $config The configuration to move the instance to. + * @type string $displayName **Defaults to** the value of $name. + * @type int $nodeCount **Defaults to** `1`. + * @type array $labels For more information, see + * [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). * } * @return void * @throws \InvalidArgumentException @@ -238,29 +243,13 @@ public function update(array $options = []) $options += [ 'displayName' => $info['displayName'], 'nodeCount' => $info['nodeCount'], - 'config' => null, 'labels' => (isset($info['labels'])) ? $info['labels'] : [] ]; - $config = $info['config']; - if ($options['config']) { - if (!($options['config'] instanceof Configuration)) { - throw new \InvalidArgumentException( - 'Given configuration is not an instance of Configuration.' - ); - } - - $config = InstanceAdminClient::formatInstanceConfigName( - $this->projectId, - $options['config']->name() - ); - } - $this->connection->updateInstance([ 'name' => $this->fullyQualifiedInstanceName(), - 'config' => $config, ] + $options); } @@ -308,7 +297,7 @@ public function createDatabase($name, array $options = []) $statement = sprintf('CREATE DATABASE `%s`', $name); - $res = $this->connection->createDatabase([ + $this->connection->createDatabase([ 'instance' => $this->fullyQualifiedInstanceName(), 'createStatement' => $statement, 'extraStatements' => $options['statements'] @@ -347,8 +336,6 @@ public function database($name) * $databases = $instance->databases(); * ``` * - * @todo implement pagination! - * * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/list List Databases * * @param array $options Configuration options. @@ -356,18 +343,29 @@ public function database($name) */ public function databases(array $options = []) { - $res = $this->connection->listDatabases($options + [ - 'instance' => $this->fullyQualifiedInstanceName(), - ]); + $pageToken = null; + + do { + $res = $this->connection->listDatabases($options + [ + 'instance' => $this->fullyQualifiedInstanceName(), + 'pageToken' => $pageToken + ]); + + $databases = []; + if (isset($res['databases'])) { + foreach ($res['databases'] as $database) { + yield $this->database( + DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']) + ); + } + } - $databases = []; - if (isset($res['databases'])) { - foreach ($res['databases'] as $database) { - yield $this->database( - DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']) - ); + if (isset($res['nextPageToken'])) { + $pageToken = $res['nextPageToken']; + } else { + $pageToken = null; } - } + } while($pageToken); } /** diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 0e05dd90f20a..63431da2af4e 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -42,23 +42,14 @@ class Operation */ private $connection; - /** - * @var Instance - */ - private $instance; - /** * @param ConnectionInterface $connection A connection to Google Cloud * Spanner. - * @param Instance $instance The current Cloud Spanner instance. */ public function __construct( - ConnectionInterface $connection, - Instance $instance, - Database $database + ConnectionInterface $connection ) { $this->connection = $connection; - $this->instance = $instance; } /** @@ -224,7 +215,6 @@ public function __debugInfo() { return [ 'connection' => get_class($this->connection), - 'instance' => $this->instance, 'sessionPool' => $this->sessionPool ]; } diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 3be6db3aeb0d..8d7cb0dea336 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -141,6 +141,8 @@ public function configurations(array $options = []) if (isset($res['nextPageToken'])) { $pageToken = $res['nextPageToken']; + } else { + $pageToken = null; } } while($pageToken); } @@ -173,7 +175,7 @@ public function configuration($name, array $config = []) * * Example: * ``` - * $instance = $spanner->createInstance($configuration, 'my-application-instance'); + * $instance = $spanner->createInstance($configuration, 'my-instance'); * ``` * * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/create Create Instance @@ -186,7 +188,6 @@ public function configuration($name, array $config = []) * * @type string $displayName **Defaults to** the value of $name. * @type int $nodeCount **Defaults to** `1`. - * @type int $state **Defaults to** * @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). * } @@ -198,10 +199,12 @@ public function createInstance(Configuration $config, $name, array $options = [] $options += [ 'displayName' => $name, 'nodeCount' => self::DEFAULT_NODE_COUNT, - 'state' => State::CREATING, 'labels' => [] ]; + // This must always be set to CREATING, so overwrite anything else. + $options['state'] = State::CREATING; + $res = $this->connection->createInstance([ 'instanceId' => $name, 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), @@ -217,7 +220,7 @@ public function createInstance(Configuration $config, $name, array $options = [] * * Example: * ``` - * $instance = $spanner->instance('my-application-instance'); + * $instance = $spanner->instance('my-instance'); * ``` * * @param string $name The instance name @@ -234,29 +237,6 @@ public function instance($name, array $instance = []) ); } - /** - * Connect to a database to run queries or mutations. - * - * Example: - * ``` - * $database = $spanner->connect('my-application-instance', 'my-application-database'); - * ``` - * - * @param Instance|string $instance The instance object or instance name. - * @param string $name The database name. - * @return Database - */ - public function connect($instance, $name) - { - if (is_string($instance)) { - $instance = $this->instance($instance); - } - - $database = $instance->database($name); - - return $database; - } - /** * List instances in the project * @@ -279,7 +259,7 @@ public function instances(array $options = []) ]; $res = $this->connection->listInstances($options + [ - 'projectId' => $this->projectId, + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), ]); if (isset($res['instances'])) { @@ -292,6 +272,29 @@ public function instances(array $options = []) } } + /** + * Connect to a database to run queries or mutations. + * + * Example: + * ``` + * $database = $spanner->connect('my-instance', 'my-application-database'); + * ``` + * + * @param Instance|string $instance The instance object or instance name. + * @param string $name The database name. + * @return Database + */ + public function connect($instance, $name) + { + if (is_string($instance)) { + $instance = $this->instance($instance); + } + + $database = $instance->database($name); + + return $database; + } + /** * Create a new KeySet object * diff --git a/tests/snippets/Spanner/Admin/ConfigurationTest.php b/tests/snippets/Spanner/Admin/ConfigurationTest.php new file mode 100644 index 000000000000..530ddac3898b --- /dev/null +++ b/tests/snippets/Spanner/Admin/ConfigurationTest.php @@ -0,0 +1,119 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->config = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), + self::PROJECT, + self::CONFIG + ]); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Configuration::class); + $res = $snippet->invoke('configuration'); + + $this->assertInstanceOf(Configuration::class, $res->returnVal()); + $this->assertEquals(self::CONFIG, $res->returnVal()->name()); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'name'); + $snippet->addLocal('configuration', $this->config); + + $res = $snippet->invoke('name'); + $this->assertEquals(self::CONFIG, $res->returnVal()); + } + + public function testInfo() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'info'); + $snippet->addLocal('configuration', $this->config); + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]); + + $this->config->setConnection($this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals(self::CONFIG, $res->output()); + } + + public function testExists() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'exists'); + $snippet->addLocal('configuration', $this->config); + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]); + + $this->config->setConnection($this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Configuration exists!', $res->output()); + } + + public function testReload() + { + $info = [ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]; + + $snippet = $this->snippetFromMethod(Configuration::class, 'reload'); + $snippet->addLocal('configuration', $this->config); + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn($info); + + $this->config->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('info'); + $this->assertEquals($info, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/Admin/DatabaseTest.php b/tests/snippets/Spanner/Admin/DatabaseTest.php new file mode 100644 index 000000000000..5ed75e6d988b --- /dev/null +++ b/tests/snippets/Spanner/Admin/DatabaseTest.php @@ -0,0 +1,147 @@ +prophesize(Instance::class); + $instance->name()->willReturn(self::INSTANCE); + + $this->connection = $this->prophesize(ConnectionInterface::class); + $this->database = \Google\Cloud\Dev\stub(Database::class, [ + $this->connection->reveal(), + $instance->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), + self::PROJECT, + self::DATABASE + ]); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Database::class, 'name'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke('name'); + $this->assertEquals(self::DATABASE, $res->returnVal()); + } + + public function testExists() + { + $snippet = $this->snippetFromMethod(Database::class, 'exists'); + $snippet->addLocal('database', $this->database); + + $this->connection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willReturn(['statements' => []]); + + $this->database->setConnection($this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Database exists!', $res->output()); + } + + public function testUpdateDdl() + { + $snippet = $this->snippetFromMethod(Database::class, 'updateDdl'); + $snippet->addLocal('database', $this->database); + + $this->connection->updateDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->setConnection($this->connection->reveal()); + + $snippet->invoke(); + } + + public function testUpdateDdlBatch() + { + $snippet = $this->snippetFromMethod(Database::class, 'updateDdlBatch'); + $snippet->addLocal('database', $this->database); + + $this->connection->updateDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->setConnection($this->connection->reveal()); + + $snippet->invoke(); + } + + public function testDrop() + { + $snippet = $this->snippetFromMethod(Database::class, 'drop'); + $snippet->addLocal('database', $this->database); + + $this->connection->dropDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->setConnection($this->connection->reveal()); + + $snippet->invoke(); + } + + public function testDdl() + { + $snippet = $this->snippetFromMethod(Database::class, 'ddl'); + $snippet->addLocal('database', $this->database); + + $stmts = [ + 'CREATE TABLE TestSuites', + 'CREATE TABLE TestCases' + ]; + + $this->connection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'statements' => $stmts + ]); + + $this->database->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('statements'); + $this->assertEquals($stmts, $res->returnVal()); + } + + public function testIam() + { + $snippet = $this->snippetFromMethod(Database::class, 'iam'); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke('iam'); + $this->assertInstanceOf(Iam::class, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/Admin/InstanceTest.php b/tests/snippets/Spanner/Admin/InstanceTest.php new file mode 100644 index 000000000000..e56274946b6f --- /dev/null +++ b/tests/snippets/Spanner/Admin/InstanceTest.php @@ -0,0 +1,215 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->instance = \Google\Cloud\Dev\stub(Instance::class, [ + $this->connection->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), + self::PROJECT, + self::INSTANCE + ]); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Instance::class); + $res = $snippet->invoke('instance'); + $this->assertInstanceOf(Instance::class, $res->returnVal()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Instance::class, 'name'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('name'); + $this->assertEquals(self::INSTANCE, $res->returnVal()); + } + + public function testInfo() + { + $snippet = $this->snippetFromMethod(Instance::class, 'info'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn(['nodeCount' => 1]); + + $this->instance->setConnection($this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('1', $res->output()); + } + + public function testExists() + { + $snippet = $this->snippetFromMethod(Instance::class, 'exists'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn(['foo' => 'bar']); + + $this->instance->setConnection($this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Instance exists!', $res->output()); + } + + public function testReload() + { + $snippet = $this->snippetFromMethod(Instance::class, 'reload'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn(['nodeCount' => 1]); + + $this->instance->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('info'); + $info = $this->instance->info(); + $this->assertEquals($info, $res->returnVal()); + } + + public function testState() + { + $snippet = $this->snippetFromMethod(Instance::class, 'state'); + $snippet->addLocal('instance', $this->instance); + $snippet->addUse(Instance::class); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn(['state' => Instance::STATE_READY]); + + $this->instance->setConnection($this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Instance is ready!', $res->output()); + } + + public function testUpdate() + { + $snippet = $this->snippetFromMethod(Instance::class, 'update'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn([ + 'displayName' => 'foo', + 'nodeCount' => 1 + ]); + + $this->connection->updateInstance(Argument::any()) + ->shouldBeCalled(); + + $this->instance->setConnection($this->connection->reveal()); + $snippet->invoke(); + } + + public function testDelete() + { + $snippet = $this->snippetFromMethod(Instance::class, 'delete'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->deleteInstance(Argument::any()) + ->shouldBeCalled(); + + $this->instance->setConnection($this->connection->reveal()); + $snippet->invoke(); + } + + public function testCreateDatabase() + { + $snippet = $this->snippetFromMethod(Instance::class, 'createDatabase'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->createDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->instance->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + } + + public function testDatabase() + { + $snippet = $this->snippetFromMethod(Instance::class, 'database'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + } + + public function databases() + { + $snippet = $this->snippetFromMethod(Instance::class, 'databases'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->listDatabases(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'databases' => [ + 'projects/'. self::PROJECT .'/instances/'. self::INSTANCE .'/database/'. self::DATABASE + ] + ]); + + $this->instance->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('databases'); + + $this->assertInstanceOf(\Generator::class, $res->returnVal()); + $this->assertInstanceOf(Database::class, $res->returnVal()->current()); + } + + public function testIam() + { + $snippet = $this->snippetFromMethod(Instance::class, 'iam'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('iam'); + $this->assertInstanceOf(Iam::class, $res->returnVal()); + } +} diff --git a/tests/snippets/Spanner/Admin/SpannerClientTest.php b/tests/snippets/Spanner/Admin/SpannerClientTest.php new file mode 100644 index 000000000000..b4e258c6e250 --- /dev/null +++ b/tests/snippets/Spanner/Admin/SpannerClientTest.php @@ -0,0 +1,129 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->client = \Google\Cloud\Dev\stub(SpannerClient::class); + $this->client->setConnection($this->connection->reveal()); + } + + public function testConfigurations() + { + $this->connection->listConfigs(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instanceConfigs' => [ + ['name' => 'projects/my-awesome-projects/instanceConfigs/Foo'], + ['name' => 'projects/my-awesome-projects/instanceConfigs/Bar'], + ] + ]); + + $this->client->setConnection($this->connection->reveal()); + + $snippet = $this->snippetFromMethod(SpannerClient::class, 'configurations'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('configurations'); + + $this->assertInstanceOf(\Generator::class, $res->returnVal()); + $this->assertInstanceOf(Configuration::class, $res->returnVal()->current()); + $this->assertEquals('Foo', $res->returnVal()->current()->name()); + } + + public function testConfiguration() + { + $configName = 'foo'; + + $snippet = $this->snippetFromMethod(SpannerClient::class, 'configuration'); + $snippet->addLocal('spanner', $this->client); + $snippet->addLocal('configurationName', self::CONFIG); + + $res = $snippet->invoke('configuration'); + $this->assertInstanceOf(Configuration::class, $res->returnVal()); + $this->assertEquals(self::CONFIG, $res->returnVal()->name()); + } + + public function testCreateInstance() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'createInstance'); + $snippet->addLocal('spanner', $this->client); + $snippet->addLocal('configuration', $this->client->configuration(self::CONFIG)); + + $this->connection->createInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn([]); + + $this->client->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('instance'); + $this->assertInstanceOf(Instance::class, $res->returnVal()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + } + + public function testInstance() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'instance'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('instance'); + $this->assertInstanceOf(Instance::class, $res->returnVal()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + } + + public function testInstances() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'instances'); + $snippet->addLocal('spanner', $this->client); + + $this->connection->listInstances(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instances' => [ + ['name' => 'projects/my-awesome-project/instances/'. self::INSTANCE], + ['name' => 'projects/my-awesome-project/instances/Bar'] + ] + ]); + + $this->client->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('instances'); + $this->assertInstanceOf(\Generator::class, $res->returnVal()); + $this->assertInstanceOf(Instance::class, $res->returnVal()->current()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->current()->name()); + } +} diff --git a/tests/unit/Spanner/Admin/ConfigurationTest.php b/tests/unit/Spanner/Admin/ConfigurationTest.php index b6e388e7548b..1edd032654af 100644 --- a/tests/unit/Spanner/Admin/ConfigurationTest.php +++ b/tests/unit/Spanner/Admin/ConfigurationTest.php @@ -24,7 +24,7 @@ use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class ConfigurationTest extends \PHPUnit_Framework_TestCase { diff --git a/tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php b/tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php index 10d518dcfa3e..aaf664150aad 100644 --- a/tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php +++ b/tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php @@ -22,7 +22,7 @@ use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class IamDatabaseTest extends \PHPUnit_Framework_TestCase { diff --git a/tests/unit/Spanner/Admin/Connection/IamInstanceTest.php b/tests/unit/Spanner/Admin/Connection/IamInstanceTest.php index a979ed863df9..7bb4377797f9 100644 --- a/tests/unit/Spanner/Admin/Connection/IamInstanceTest.php +++ b/tests/unit/Spanner/Admin/Connection/IamInstanceTest.php @@ -22,7 +22,7 @@ use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class IamInstanceTest extends \PHPUnit_Framework_TestCase { diff --git a/tests/unit/Spanner/Admin/DatabaseTest.php b/tests/unit/Spanner/Admin/DatabaseTest.php index 655f26172b92..6407ab905573 100644 --- a/tests/unit/Spanner/Admin/DatabaseTest.php +++ b/tests/unit/Spanner/Admin/DatabaseTest.php @@ -27,7 +27,7 @@ use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class DatabaseTest extends \PHPUnit_Framework_TestCase { diff --git a/tests/unit/Spanner/Admin/InstanceTest.php b/tests/unit/Spanner/Admin/InstanceTest.php index 85e6b6f0c5c3..b9a4d1484907 100644 --- a/tests/unit/Spanner/Admin/InstanceTest.php +++ b/tests/unit/Spanner/Admin/InstanceTest.php @@ -29,7 +29,7 @@ use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class InstanceTest extends \PHPUnit_Framework_TestCase { @@ -151,7 +151,6 @@ public function testUpdate() 'displayName' => $instance['displayName'], 'nodeCount' => $instance['nodeCount'], 'labels' => [], - 'config' => $instance['config'] ])->shouldBeCalled(); $this->instance->setConnection($this->connection->reveal()); @@ -173,7 +172,6 @@ public function testUpdateWithExistingLabels() 'displayName' => $instance['displayName'], 'nodeCount' => $instance['nodeCount'], 'labels' => $instance['labels'], - 'config' => $instance['config'] ])->shouldBeCalled(); $this->instance->setConnection($this->connection->reveal()); @@ -185,16 +183,12 @@ public function testUpdateWithChanges() { $instance = $this->getDefaultInstance(); - $config = $this->prophesize(Configuration::class); - $config->name()->willReturn('config-name'); - $changes = [ 'labels' => [ 'foo' => 'bar' ], 'nodeCount' => 900, 'displayName' => 'New Name', - 'config' => $config->reveal() ]; $this->connection->getInstance(Argument::any()) @@ -206,7 +200,6 @@ public function testUpdateWithChanges() 'displayName' => $changes['displayName'], 'nodeCount' => $changes['nodeCount'], 'labels' => $changes['labels'], - 'config' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, $changes['config']->name()) ])->shouldBeCalled(); $this->instance->setConnection($this->connection->reveal()); @@ -214,26 +207,6 @@ public function testUpdateWithChanges() $this->instance->update($changes); } - /** - * @expectedException InvalidArgumentException - */ - public function testUpdateInvalidConfig() - { - $instance = $this->getDefaultInstance(); - - $changes = [ - 'config' => 'foo' - ]; - - $this->connection->getInstance(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - - $this->instance->setConnection($this->connection->reveal()); - - $this->instance->update($changes); - } - public function testDelete() { $this->connection->deleteInstance([ diff --git a/tests/unit/Spanner/Admin/SpannerClientTest.php b/tests/unit/Spanner/Admin/SpannerClientTest.php index 295ff6318ace..4d38098f9e38 100644 --- a/tests/unit/Spanner/Admin/SpannerClientTest.php +++ b/tests/unit/Spanner/Admin/SpannerClientTest.php @@ -25,7 +25,7 @@ use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class SpannerClientTest extends \PHPUnit_Framework_TestCase { @@ -69,6 +69,43 @@ public function testConfigurations() $this->assertInstanceOf(Configuration::class, $configs[1]); } + public function testPagedConfigurations() + { + $firstCall = [ + 'instanceConfigs' => [ + [ + 'name' => 'projects/foo/instanceConfigs/bar', + 'displayName' => 'Bar' + ] + ], + 'nextPageToken' => 'fooBar' + ]; + + $secondCall = [ + 'instanceConfigs' => [ + [ + 'name' => 'projects/foo/instanceConfigs/bat', + 'displayName' => 'Bat' + ] + ] + ]; + + $this->connection->listConfigs(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn($firstCall, $secondCall); + + $this->client->setConnection($this->connection->reveal()); + + $configs = $this->client->configurations(); + + $this->assertInstanceOf(\Generator::class, $configs); + + $configs = iterator_to_array($configs); + $this->assertEquals(2, count($configs)); + $this->assertInstanceOf(Configuration::class, $configs[0]); + $this->assertInstanceOf(Configuration::class, $configs[1]); + } + public function testConfiguration() { $config = $this->client->configuration('bar'); From 81fee5171dd9a17cfdb69acf002a6e3005749377 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 21 Dec 2016 13:51:21 -0500 Subject: [PATCH 014/107] Grpc tests except for create/update instance --- .../Spanner/Admin/Connection/GrpcTest.php | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 tests/unit/Spanner/Admin/Connection/GrpcTest.php diff --git a/tests/unit/Spanner/Admin/Connection/GrpcTest.php b/tests/unit/Spanner/Admin/Connection/GrpcTest.php new file mode 100644 index 000000000000..f859e5d8aae3 --- /dev/null +++ b/tests/unit/Spanner/Admin/Connection/GrpcTest.php @@ -0,0 +1,188 @@ +markTestSkipped('Must have the grpc extension installed to run this test.'); + } + + $this->requestWrapper = $this->prophesize(GrpcRequestWrapper::class); + $this->successMessage = 'success'; + } + + /** + * @dataProvider methodProvider + */ + public function testCallBasicMethods($method, $args, $expectedArgs) + { + $this->requestWrapper->send( + Argument::type('callable'), + $expectedArgs, + Argument::type('array') + )->willReturn($this->successMessage); + + $grpc = new Grpc(); + $grpc->setRequestWrapper($this->requestWrapper->reveal()); + + $this->assertEquals($this->successMessage, $grpc->$method($args)); + } + + public function methodProvider() + { + $configName = 'test-config'; + $instanceName = 'muh-instance'; + + $instanceArgs = [ + 'name' => $instanceName, + 'config' => 'foo', + 'displayName' => 'instanceName', + 'nodeCount' => 2, + 'labels' => [], + 'instanceId' => $instanceName, + 'state' => null, + ]; + + $databaseName = 'foo'; + $createStmt = 'CREATE DATABASE foo'; + $extraStmts = ['CREATE TABLE bar']; + + return [ + [ + 'listConfigs', + ['projectId' => self::PROJECT], + [self::PROJECT, []] + ], + [ + 'getConfig', + ['name' => $configName], + [$configName, []] + ], + [ + 'listInstances', + ['projectId' => self::PROJECT], + [self::PROJECT, []] + ], + [ + 'getInstance', + ['name' => $instanceName], + [$instanceName, []] + ], + // [ + // 'createInstance', + // $instanceArgs + ['projectId' => self::PROJECT], + // [self::PROJECT, $instanceName, Argument::type(\google\spanner\admin\instance\v1\Instance::class), []] + // ], + // [ + // 'updateInstance', + // ['name' => $value], + // [$value, []] + // ], + [ + 'deleteInstance', + ['name' => $instanceName], + [$instanceName, []] + ], + [ + 'getInstanceIamPolicy', + ['resource' => $instanceName], + [$instanceName, []] + ], + [ + 'setInstanceIamPolicy', + ['resource' => $instanceName, 'policy' => 'foo'], + [$instanceName, 'foo', []] + ], + [ + 'testInstanceIamPermissions', + ['resource' => $instanceName, 'permissions' => ['foo']], + [$instanceName, ['foo'], []] + ], + [ + 'listDatabases', + ['instance' => $instanceName], + [$instanceName, []] + ], + [ + 'createDatabase', + [ + 'instance' => $instanceName, + 'createStatement' => $createStmt, + 'extraStatements' => $extraStmts], + [$instanceName, $createStmt, $extraStmts, []] + ], + [ + 'updateDatabase', + [ + 'name' => $databaseName, + 'statements' => $extraStmts + ], + [$databaseName, $extraStmts, []] + ], + [ + 'dropDatabase', + ['name' => $databaseName], + [$databaseName, []] + ], + [ + 'getDatabaseDDL', + ['name' => $databaseName], + [$databaseName, []] + ], + [ + 'getDatabaseIamPolicy', + ['resource' => $databaseName], + [$databaseName, []] + ], + [ + 'setDatabaseIamPolicy', + [ + 'resource' => $databaseName, + 'policy' => 'foo' + ], + [$databaseName, 'foo', []] + ], + [ + 'testDatabaseIamPermissions', + [ + 'resource' => $databaseName, + 'permissions' => 'foo' + ], + [$databaseName, 'foo', []] + ], + ]; + } +} From e4162a013d9a896c9792114e3aa57a4799188943 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 21 Dec 2016 17:40:54 -0500 Subject: [PATCH 015/107] Add Grpc unit tests --- .../Spanner/Admin/Connection/GrpcTest.php | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/tests/unit/Spanner/Admin/Connection/GrpcTest.php b/tests/unit/Spanner/Admin/Connection/GrpcTest.php index f859e5d8aae3..ebfe45f19e6e 100644 --- a/tests/unit/Spanner/Admin/Connection/GrpcTest.php +++ b/tests/unit/Spanner/Admin/Connection/GrpcTest.php @@ -19,6 +19,7 @@ use Google\Cloud\GrpcRequestWrapper; use Google\Cloud\GrpcTrait; +use Google\Cloud\PhpArray; use Google\Cloud\Spanner\Connection\Grpc; use Prophecy\Argument; @@ -71,10 +72,14 @@ public function methodProvider() 'config' => 'foo', 'displayName' => 'instanceName', 'nodeCount' => 2, - 'labels' => [], - 'instanceId' => $instanceName, - 'state' => null, + 'state' => null ]; + $instance = (new \google\spanner\admin\instance\v1\Instance())->deserialize($instanceArgs, new PhpArray()); + $fieldMask = (new \google\protobuf\FieldMask())->deserialize([ + 'paths' => [ + 'name', 'config', 'display_name', 'node_count' + ] + ], new PhpArray()); $databaseName = 'foo'; $createStmt = 'CREATE DATABASE foo'; @@ -101,16 +106,22 @@ public function methodProvider() ['name' => $instanceName], [$instanceName, []] ], - // [ - // 'createInstance', - // $instanceArgs + ['projectId' => self::PROJECT], - // [self::PROJECT, $instanceName, Argument::type(\google\spanner\admin\instance\v1\Instance::class), []] - // ], - // [ - // 'updateInstance', - // ['name' => $value], - // [$value, []] - // ], + [ + 'createInstance', + $instanceArgs + [ + 'projectId' => self::PROJECT, + 'instanceId' => $instanceName, + 'labels' => [] + ], + [self::PROJECT, $instanceName, $instance, []] + ], + [ + 'updateInstance', + $instanceArgs + [ + 'labels' => [] + ], + [$instance, $fieldMask, []] + ], [ 'deleteInstance', ['name' => $instanceName], From 8a329f7b86ec561ffd08ee3a55691469a70a1327 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Thu, 22 Dec 2016 09:08:44 -0500 Subject: [PATCH 016/107] Fix fixture --- tests/unit/Spanner/Admin/InstanceTest.php | 2 +- tests/{ => unit}/fixtures/spanner/instance.json | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/{ => unit}/fixtures/spanner/instance.json (100%) diff --git a/tests/unit/Spanner/Admin/InstanceTest.php b/tests/unit/Spanner/Admin/InstanceTest.php index b9a4d1484907..b8e600942ece 100644 --- a/tests/unit/Spanner/Admin/InstanceTest.php +++ b/tests/unit/Spanner/Admin/InstanceTest.php @@ -284,6 +284,6 @@ public function testIam() private function getDefaultInstance() { - return json_decode(file_get_contents(__DIR__ .'/../../../fixtures/spanner/instance.json'), true); + return json_decode(file_get_contents(__DIR__ .'/../../fixtures/spanner/instance.json'), true); } } diff --git a/tests/fixtures/spanner/instance.json b/tests/unit/fixtures/spanner/instance.json similarity index 100% rename from tests/fixtures/spanner/instance.json rename to tests/unit/fixtures/spanner/instance.json From 9ae9868c9d90bba57061a7610eae4ab46bf8ec94 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 23 Dec 2016 09:56:12 -0500 Subject: [PATCH 017/107] fixes --- src/Spanner/Connection/Grpc.php | 3 ++- src/Spanner/Instance.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index d0be511e6372..089b1dfd2cf5 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -135,10 +135,11 @@ public function getInstance(array $args = []) */ public function createInstance(array $args = []) { + $instance = $this->instanceObject($args, true); return $this->send([$this->instanceAdminClient, 'createInstance'], [ $this->pluck('projectId', $args), $this->pluck('instanceId', $args), - $this->instanceObject($args, true), + $instance, $args ]); } diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 4a741bf498a2..7e99c7ecb776 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -242,7 +242,7 @@ public function update(array $options = []) $options += [ 'displayName' => $info['displayName'], - 'nodeCount' => $info['nodeCount'], + 'nodeCount' => (isset($info['nodeCount'])) ? $info['nodeCount'] : null, 'labels' => (isset($info['labels'])) ? $info['labels'] : [] From 08c3ca97eadb459c6b90f8ce2532e095d571370f Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 23 Dec 2016 10:17:56 -0500 Subject: [PATCH 018/107] Address code review comments --- src/Logging/Connection/Grpc.php | 20 +++++++++++--------- src/PhpArray.php | 17 +++++++++++------ src/PubSub/Connection/Grpc.php | 10 +++++++--- src/ServiceBuilder.php | 2 +- src/Spanner/Configuration.php | 4 ++-- src/Spanner/Connection/Grpc.php | 15 +++++++-------- src/Spanner/Database.php | 18 ++++++++++++++++-- src/Spanner/Instance.php | 32 ++++++++++++++++++++++---------- src/Spanner/SpannerClient.php | 8 +++----- tests/unit/PhpArrayTest.php | 2 +- 10 files changed, 81 insertions(+), 47 deletions(-) diff --git a/src/Logging/Connection/Grpc.php b/src/Logging/Connection/Grpc.php index b9fc8f6d43dd..183b50beb1a9 100644 --- a/src/Logging/Connection/Grpc.php +++ b/src/Logging/Connection/Grpc.php @@ -89,15 +89,17 @@ class Grpc implements ConnectionInterface public function __construct(array $config = []) { $this->codec = new PhpArray([ - 'timestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - }, - 'severity' => function ($v) { - return Logger::getLogLevelMap()[$v]; - }, - 'outputVersionFormat' => function ($v) { - return self::$versionFormatMap[$v]; - } + 'customFilters' => [ + 'timestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + }, + 'severity' => function ($v) { + return Logger::getLogLevelMap()[$v]; + }, + 'outputVersionFormat' => function ($v) { + return self::$versionFormatMap[$v]; + } + ] ]); $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); diff --git a/src/PhpArray.php b/src/PhpArray.php index f2a4029b368f..9fb1b215067c 100644 --- a/src/PhpArray.php +++ b/src/PhpArray.php @@ -38,17 +38,22 @@ class PhpArray extends Protobuf\Codec\PhpArray private $useCamelCase; /** - * @param array $customFilters A set of callbacks to apply to properties in + * @param array $config [optional] { + * Configuration Options + * + * @type array $customFilters A set of callbacks to apply to properties in * a gRPC response. + * @type bool $useCamelCase Whether to convert key casing to camelCase. + * } */ - public function __construct(array $customFilters = [], array $config = []) + public function __construct(array $config = []) { - $this->customFilters = $customFilters; - - $config = $config + [ - 'useCamelCase' => true + $config += [ + 'useCamelCase' => true, + 'customFilters' => [] ]; + $this->customFilters = $config['customFilters']; $this->useCamelCase = $config['useCamelCase']; } diff --git a/src/PubSub/Connection/Grpc.php b/src/PubSub/Connection/Grpc.php index 3731bfb88812..84b11c04a941 100644 --- a/src/PubSub/Connection/Grpc.php +++ b/src/PubSub/Connection/Grpc.php @@ -60,9 +60,13 @@ class Grpc implements ConnectionInterface */ public function __construct(array $config = []) { - $this->codec = new PhpArray(['publishTime' => function ($v) { - return $this->formatTimestampFromApi($v); - }]); + $this->codec = new PhpArray([ + 'customFilters' => [ + 'publishTime' => function ($v) { + return $this->formatTimestampFromApi($v); + } + ] + ]); $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); $grpcConfig = $this->getGaxConfig(); diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index a0519c357a0c..998a51782fd1 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -213,7 +213,7 @@ public function pubsub(array $config = []) /** * Google Cloud Spanner client. Google Cloud Spanner is a highly scalable, * transactional, managed, NewSQL database service. Find more information - * at [Google Cloud Spanner API docs](https://cloud.google.com/spanner/). + * at [Google Cloud Spanner API docs](https://cloud.google.com/spanner/). * * Example: * ``` diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php index 6d59c1bd748a..4a51b5575417 100644 --- a/src/Spanner/Configuration.php +++ b/src/Spanner/Configuration.php @@ -108,7 +108,7 @@ public function name() * * @codingStandardsIgnoreStart * @param array $options [optional] Configuration options. - * @return array [https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs#InstanceConfig](InstanceConfig) + * @return array [InstanceConfig](https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs#InstanceConfig) * @codingStandardsIgnoreEnd */ public function info(array $options = []) @@ -156,7 +156,7 @@ public function exists(array $options = []) * * @codingStandardsIgnoreStart * @param array $options [optional] Configuration options. - * @return array [https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs#InstanceConfig](InstanceConfig) + * @return array [InstanceConfig](https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs#InstanceConfig) * @codingStandardsIgnoreEnd */ public function reload(array $options = []) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 089b1dfd2cf5..baa7f505fcf4 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -72,9 +72,11 @@ class Grpc implements ConnectionInterface public function __construct(array $config = []) { $this->codec = new PhpArray([ - 'timestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - } + 'customFilters' => [ + 'timestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + } + ] ]); $config['codec'] = $this->codec; @@ -151,12 +153,9 @@ public function updateInstance(array $args = []) { $instanceObject = $this->instanceObject($args); - $mask = array_keys($instanceObject->serialize(new PhpArray([], ['useCamelCase' => false]))); + $mask = array_keys($instanceObject->serialize(new PhpArray(['useCamelCase' => false]))); - $fieldMask = new protobuf\FieldMask(); - array_walk($mask, function (&$element) use ($fieldMask) { - $fieldMask->addPaths($element); - }); + $fieldMask = (new protobuf\FieldMask())->deserialize(['paths' => $mask], $this->codec); return $this->send([$this->instanceAdminClient, 'updateInstance'], [ $instanceObject, diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 7e0498cfd531..f113ad3d89d2 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -170,15 +170,18 @@ public function exists(array $options = []) * ); * ``` * + * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.UpdateDatabaseDdlRequest UpdateDDLRequest + * @codingStandardsIgnoreEnd * * @param string $statement A DDL statement to run against a database. * @param array $options [optional] Configuration options. * @return */ - public function updateDdl($statements, array $options = []) + public function updateDdl($statement, array $options = []) { - return $this->updateDdlBatch([$statements], $options); + return $this->updateDdlBatch([$statement], $options); } /** @@ -200,7 +203,10 @@ public function updateDdl($statements, array $options = []) * ]); * ``` * + * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.UpdateDatabaseDdlRequest UpdateDDLRequest + * @codingStandardsIgnoreEnd * * @param string[] $statements A list of DDL statements to run against a database. * @param array $options [optional] Configuration options. @@ -226,6 +232,10 @@ public function updateDdlBatch(array $statements, array $options = []) * $database->drop(); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DropDatabaseRequest DropDatabaseRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration options. * @return void */ @@ -244,6 +254,10 @@ public function drop(array $options = []) * $statements = $database->ddl(); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#getdatabaseddlrequest GetDatabaseDdlRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration options. * @return array */ diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 7e99c7ecb776..96ec6c8f646c 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -174,6 +174,10 @@ public function exists(array $options = []) * $info = $instance->reload(); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.GetInstanceRequest GetInstanceRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration options. * @return array */ @@ -223,15 +227,19 @@ public function state(array $options = []) * ]); * ``` * - * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1 Update Instance + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#updateinstancerequest UpdateInstanceRequest + * @codingStandardsIgnoreEnd * * @param array $options [optional] { * Configuration options * - * @type string $displayName **Defaults to** the value of $name. - * @type int $nodeCount **Defaults to** `1`. + * @type string $displayName The descriptive name for this instance as + * it appears in UIs. **Defaults to** the value of $name. + * @type int $nodeCount The number of nodes allocated to this instance. + * **Defaults to** `1`. * @type array $labels For more information, see - * [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). + * [Using labels to organize Google Cloud Platform resources](https://goo.gl/xmQnxf). * } * @return void * @throws \InvalidArgumentException @@ -261,6 +269,10 @@ public function update(array $options = []) * $instance->delete(); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#deleteinstancerequest DeleteInstanceRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration options. * @return void */ @@ -279,7 +291,9 @@ public function delete(array $options = []) * $database = $instance->createDatabase('my-database'); * ``` * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/create Create Database + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#createdatabaserequest CreateDatabaseRequest + * @codingStandardsIgnoreEnd * * @param string $name The database name. * @param array $options [optional] { @@ -360,11 +374,9 @@ public function databases(array $options = []) } } - if (isset($res['nextPageToken'])) { - $pageToken = $res['nextPageToken']; - } else { - $pageToken = null; - } + $pageToken = (isset($res['nextPageToken'])) + ? $res['nextPageToken'] + : null; } while($pageToken); } diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 8d7cb0dea336..3b0fe0dbdb4e 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -139,11 +139,9 @@ public function configurations(array $options = []) } } - if (isset($res['nextPageToken'])) { - $pageToken = $res['nextPageToken']; - } else { - $pageToken = null; - } + $pageToken = (isset($res['nextPageToken'])) + ? $res['nextPageToken'] + : null; } while($pageToken); } diff --git a/tests/unit/PhpArrayTest.php b/tests/unit/PhpArrayTest.php index 4df80c571925..11188fdd7075 100644 --- a/tests/unit/PhpArrayTest.php +++ b/tests/unit/PhpArrayTest.php @@ -28,7 +28,7 @@ class PhpArrayTest extends \PHPUnit_Framework_TestCase { private function getCodec($customFilters = []) { - return new PhpArray($customFilters); + return new PhpArray(['customFilters' => $customFilters]); } /** From 9aadc4c95099269a2a571ca379b798cf2c4f0666 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 28 Dec 2016 15:00:56 -0500 Subject: [PATCH 019/107] Implement query parameterization and value encode/decode --- src/Exception/FailedPreconditionException.php | 27 ++ src/GrpcRequestWrapper.php | 4 + src/RequestWrapper.php | 4 + src/ServiceBuilder.php | 11 +- src/Spanner/Bytes.php | 91 ++++++ src/Spanner/Connection/Grpc.php | 9 +- src/Spanner/Database.php | 31 +- src/Spanner/Date.php | 90 ++++++ src/Spanner/Instance.php | 13 +- src/Spanner/Operation.php | 46 ++- src/Spanner/Result.php | 35 +-- src/Spanner/SpannerClient.php | 87 +++++- src/Spanner/Timestamp.php | 99 +++++++ src/Spanner/Transaction.php | 19 +- src/Spanner/ValueInterface.php | 44 +++ src/Spanner/ValueMapper.php | 271 ++++++++++++++++++ 16 files changed, 829 insertions(+), 52 deletions(-) create mode 100644 src/Exception/FailedPreconditionException.php create mode 100644 src/Spanner/Bytes.php create mode 100644 src/Spanner/Date.php create mode 100644 src/Spanner/Timestamp.php create mode 100644 src/Spanner/ValueInterface.php create mode 100644 src/Spanner/ValueMapper.php diff --git a/src/Exception/FailedPreconditionException.php b/src/Exception/FailedPreconditionException.php new file mode 100644 index 000000000000..9f0d5b6dd1f9 --- /dev/null +++ b/src/Exception/FailedPreconditionException.php @@ -0,0 +1,27 @@ +spanner(); * ``` * - * @param array $config [optional] Configuration options. See - * {@see Google\Cloud\ServiceBuilder::__construct()} for the available options. + * @param array $config [optional] { + * Configuration options. See + * {@see Google\Cloud\ServiceBuilder::__construct()} for the other available options. + * + * @type bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit + * platform compatibility. **Defaults to** false. + * } * @return SpannerClient */ public function spanner(array $config = []) diff --git a/src/Spanner/Bytes.php b/src/Spanner/Bytes.php new file mode 100644 index 000000000000..7a0710582549 --- /dev/null +++ b/src/Spanner/Bytes.php @@ -0,0 +1,91 @@ +spanner(); + * + * $bytes = $spanner->bytes('hello world'); + * ``` + */ +class Bytes implements ValueInterface +{ + /** + * @var string|resource|StreamInterface + */ + private $value; + + /** + * @param string|resource|StreamInterface $value The bytes value. + */ + public function __construct($value) + { + $this->value = Psr7\stream_for($value); + } + + /** + * Get the bytes as a stream. + * + * @return StreamInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_BYTES; + } + + /** + * Format the value as a string. + * + * @return string + */ + public function formatAsString() + { + return base64_encode((string) $this->value); + } + + /** + * Format the value as a string. + * + * @return string + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index baa7f505fcf4..1b7f6fff356b 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -31,6 +31,7 @@ use google\spanner\v1; use google\spanner\v1\Mutation; use google\spanner\v1\TransactionOptions; +use google\spanner\v1\Type; class Grpc implements ConnectionInterface { @@ -356,6 +357,11 @@ public function executeSql(array $args = []) $args['params'] = (new protobuf\Struct) ->deserialize($this->formatStructForApi($args['params']), $this->codec); + foreach ($args['paramTypes'] as $key => $param) { + $args['paramTypes'][$key] = (new Type) + ->deserialize($param, $this->codec); + } + return $this->send([$this->spannerClient, 'executeSql'], [ $this->pluck('session', $args), $this->pluck('sql', $args), @@ -471,9 +477,10 @@ public function commit(array $args = []) } if (isset($args['singleUseTransaction'])) { - $options = new TransactionOptions; $readWrite = (new TransactionOptions\ReadWrite) ->deserialize($args['singleUseTransaction']['readWrite'], $this->codec); + + $options = new TransactionOptions; $options->setReadWrite($readWrite); $args['singleUseTransaction'] = $options; } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index f113ad3d89d2..a6eee0caba46 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -94,13 +94,17 @@ class Database * @param SessionPoolInterface The session pool implementation. * @param string $projectId The project ID. * @param string $name The database name. + * @param bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit platform + * compatibility. **Defaults to** false. */ public function __construct( ConnectionInterface $connection, Instance $instance, SessionPoolInterface $sessionPool, $projectId, - $name + $name, + $returnInt64AsObject = false ) { $this->connection = $connection; $this->instance = $instance; @@ -108,7 +112,7 @@ public function __construct( $this->projectId = $projectId; $this->name = $name; - $this->operation = new Operation($connection); + $this->operation = new Operation($connection, $returnInt64AsObject); $this->iam = new Iam( new IamDatabase($this->connection), $this->fullyQualifiedDatabaseName() @@ -356,7 +360,7 @@ public function insertBatch($table, array $dataSet, array $options = []) foreach ($dataSet as $data) { $mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data); } - +// print_R($mutations);exit; $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); return $this->operation->commit($session, $mutations, $options); @@ -497,8 +501,25 @@ public function deleteBatch($table, array $keySets, array $options = []) /** * Run a query. * + * Example: + * ``` + * $result = $spanner->execute( + * 'SELECT * FROM Users WHERE id = @userId', + * [ + * 'parameters' => [ + * 'userId' => 1 + * ] + * ] + * ); + * ``` * @param string $sql The query string to execute. - * @param array $options [optional] Configuration options. + * @param array $options [optional] { + * Configuration options. + * + * @type array $parameters A key/value array of Query Parameters, where + * the key is represented in the query string prefixed by a `@` + * symbol. + * } * @return Result */ public function execute($sql, array $options = []) @@ -522,7 +543,7 @@ public function execute($sql, array $options = []) * * @type string $index The name of an index on the table. * @type array $columns A list of column names to be returned. - * @type array $keySet A [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). + * @type KeySet $keySet A [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. * } diff --git a/src/Spanner/Date.php b/src/Spanner/Date.php new file mode 100644 index 000000000000..9d74cb2fd954 --- /dev/null +++ b/src/Spanner/Date.php @@ -0,0 +1,90 @@ +spanner(); + * + * $date = $spanner->date(new \DateTime('1995-02-04')); + * ``` + */ +class Date implements ValueInterface +{ + const FORMAT = 'Y-m-d'; + + /** + * @var \DateTimeInterface + */ + protected $value; + + /** + * @param \DateTimeInterface $value The date value. + */ + public function __construct(\DateTimeInterface $value) + { + $this->value = $value; + } + + /** + * Get the underlying `\DateTimeInterface` implementation. + * + * @return \DateTimeInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_DATE; + } + + /** + * Format the value as a string. + * + * @return string + */ + public function formatAsString() + { + return $this->value->format(self::FORMAT); + } + + /** + * Format the value as a string. + * + * @return string + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 96ec6c8f646c..26658c22cba8 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -64,6 +64,11 @@ class Instance */ private $name; + /** + * @var bool + */ + private $returnInt64AsObject; + /** * @var array */ @@ -82,6 +87,9 @@ class Instance * @param SessionPoolInterface $sessionPool The session pool implementation. * @param string $projectId The project ID. * @param string $name The instance name. + * @param bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit platform + * compatibility. **Defaults to** false. * @param array $info [optional] A representation of the instance object. */ public function __construct( @@ -89,12 +97,14 @@ public function __construct( SessionPoolInterface $sessionPool, $projectId, $name, + $returnInt64AsObject = false, array $info = [] ) { $this->connection = $connection; $this->sessionPool = $sessionPool; $this->projectId = $projectId; $this->name = $name; + $this->returnInt64AsObject = $returnInt64AsObject; $this->info = $info; $this->iam = new Iam( new IamInstance($this->connection), @@ -338,7 +348,8 @@ public function database($name) $this, $this->sessionPool, $this->projectId, - $name + $name, + $this->returnInt64AsObject ); } diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 63431da2af4e..f743a514d0b8 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Spanner; +use Google\Cloud\ArrayTrait; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\ValidateTrait; @@ -30,6 +31,7 @@ */ class Operation { + use ArrayTrait; use ValidateTrait; const OP_INSERT = 'insert'; @@ -42,14 +44,22 @@ class Operation */ private $connection; + /** + * @var ValueMapper + */ + private $mapper; + /** * @param ConnectionInterface $connection A connection to Google Cloud * Spanner. + * @param bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit platform + * compatibility. */ - public function __construct( - ConnectionInterface $connection - ) { + public function __construct(ConnectionInterface $connection, $returnInt64AsObject) + { $this->connection = $connection; + $this->mapper = new ValueMapper($returnInt64AsObject); } /** @@ -63,11 +73,15 @@ public function __construct( */ public function mutation($operation, $table, $mutation) { + $mutation = array_filter($mutation, function ($value) { + return !is_null($value); + }); + return [ $operation => [ 'table' => $table, 'columns' => array_keys($mutation), - 'values' => array_values($mutation) + 'values' => $this->mapper->encodeValuesAsSimpleType(array_values($mutation)) ] ]; } @@ -149,16 +163,18 @@ public function rollback(Session $session, $transactionId, array $options = []) public function execute(Session $session, $sql, array $options = []) { $options += [ - 'params' => [], - 'paramTypes' => [] + 'parameters' => [], ]; + $parameters = $this->pluck('parameters', $options); + $options += $this->mapper->formatParamsForExecuteSql($parameters); + $res = $this->connection->executeSql([ 'sql' => $sql, 'session' => $session->name() ] + $options); - return new Result($res); + return $this->createResult($res); } /** @@ -202,7 +218,21 @@ public function read(Session $session, $table, array $options = []) 'session' => $session->name() ] + $options); - return new Result($res); + return $this->createResult($res); + } + + private function createResult(array $res) + { + $columns = $res['metadata']['rowType']['fields']; + + $rows = []; + if (isset($res['rows'])) { + foreach ($res['rows'] as $row) { + $rows[] = $this->mapper->decodeValues($columns, $row); + } + } + + return new Result($res, $rows); } /** diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index 8cab8b645b7f..e813864b7a08 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -33,12 +33,13 @@ class Result implements \IteratorAggregate private $rows; /** - * @var array $result The query or read result. + * @param array $result The query or read result. + * @param array $rows The rows, formatted and decoded. */ - public function __construct(array $result) + public function __construct(array $result, array $rows) { $this->result = $result; - $this->rows = $this->transformQueryResult($result); + $this->rows = $rows; } /** @@ -52,7 +53,7 @@ public function metadata() } /** - * Return the rows as a key/value list. + * Return the formatted and decoded rows. * * @return array|null */ @@ -88,32 +89,6 @@ public function info() return $this->result; } - /** - * Transform the response from executeSql or read into a list of rows - * represented as a collection of key/value arrays. - * - * @param array $result - * @return array - */ - private function transformQueryResult(array $result) - { - if (!isset($result['rows']) || count($result['rows']) === 0) { - return null; - } - - $cols = []; - foreach (array_keys($result['rows'][0]) as $colIndex) { - $cols[] = $result['metadata']['rowType']['fields'][$colIndex]['name']; - } - - $rows = []; - foreach ($result['rows'] as $row) { - $rows[] = array_combine($cols, $row); - } - - return $rows; - } - /** * @access private */ diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 3b0fe0dbdb4e..6383df85f2ef 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -19,6 +19,7 @@ use Google\Cloud\ClientTrait; use Google\Cloud\Exception\NotFoundException; +use Google\Cloud\Int64; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\Grpc; use Google\Cloud\Spanner\Session\SessionClient; @@ -72,6 +73,11 @@ class SpannerClient */ protected $sessionPool; + /** + * @var bool + */ + private $returnInt64AsObject; + /** * Create a Spanner client. * @@ -92,22 +98,28 @@ class SpannerClient * @type int $retries Number of retries for a failed request. * **Defaults to** `3`. * @type array $scopes Scopes to be used for the request. + * @type bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit + * platform compatibility. **Defaults to** false. * } * @throws Google\Cloud\Exception\GoogleException */ public function __construct(array $config = []) { - if (!isset($config['scopes'])) { - $config['scopes'] = [ + $config += [ + 'scopes' => [ self::FULL_CONTROL_SCOPE, self::ADMIN_SCOPE - ]; - } + ], + 'returnInt64AsObject' => false + ]; $this->connection = new Grpc($this->configureAuthentication($config)); $this->sessionClient = new SessionClient($this->connection, $this->projectId); $this->sessionPool = new SimpleSessionPool($this->sessionClient); + + $this->returnInt64AsObject = $config['returnInt64AsObject']; } /** @@ -231,6 +243,7 @@ public function instance($name, array $instance = []) $this->sessionPool, $this->projectId, $name, + $this->returnInt64AsObject, $instance ); } @@ -335,4 +348,70 @@ public function sessionClient() { return $this->sessionClient; } + + /** + * Create a Bytes object. + * + * Example: + * ``` + * $bytes = $spanner->bytes('hello world'); + * ``` + * + * @param string|resource|StreamInterface $value The bytes value. + * @return Bytes + */ + public function bytes($bytes) + { + return new Bytes($bytes); + } + + /** + * Create a Date object. + * + * Example: + * ``` + * $date = $spanner->date(new \DateTime('1995-02-04')); + * ``` + * + * @param \DateTimeInterface $value The date value. + * @return Date + */ + public function date(\DateTimeInterface $date) + { + return new Date($date); + } + + /** + * Create a Timestamp object. + * + * Example: + * ``` + * $timestamp = $spanner->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')); + * ``` + * + * @param \DateTimeInterface $value The timestamp value. + * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp. + * @return Timestamp + */ + public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null) + { + return new Timestamp($timestamp, $nanoSeconds); + } + + /** + * Create an Int64 object. This can be used to work with 64 bit integers as + * a string value while on a 32 bit platform. + * + * Example: + * ``` + * $int64 = $spanner->int64('9223372036854775807'); + * ``` + * + * @param string $value + * @return Int64 + */ + public function int64($value) + { + return new Int64($value); + } } diff --git a/src/Spanner/Timestamp.php b/src/Spanner/Timestamp.php new file mode 100644 index 000000000000..56884dc5551f --- /dev/null +++ b/src/Spanner/Timestamp.php @@ -0,0 +1,99 @@ +timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')); + * ``` + */ +class Timestamp implements ValueInterface +{ + const FORMAT = 'Y-m-d\TH:i:s.u\Z'; + const FORMAT_INTERPOLATE = 'Y-m-d\TH:i:s.%\d\Z'; + + /** + * @var \DateTimeInterface + */ + private $value; + + /** + * @var int + */ + private $nanoSeconds; + + /** + * @param \DateTimeInterface $value The timestamp value. + * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp. + */ + public function __construct(\DateTimeInterface $value, $nanoSeconds = null) + { + $this->value = $value; + $this->nanoSeconds = $nanoSeconds ?: (int) $this->value->format('u'); + } + + /** + * Get the underlying `\DateTimeInterface` implementation. + * + * @return \DateTimeInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_TIMESTAMP; + } + + /** + * Format the value as a string. + * + * @return string + */ + public function formatAsString() + { + $this->value->setTimezone(new \DateTimeZone('UTC')); + return sprintf($this->value->format(self::FORMAT_INTERPOLATE), $this->nanoSeconds); + } + + /** + * Format the value as a string. + * + * @return string + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 638330f7a515..791bcdca6664 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -170,8 +170,25 @@ public function delete($table, array $key) /** * Run a query. * + * Example: + * ``` + * $result = $spanner->execute( + * 'SELECT * FROM Users WHERE id = @userId', + * [ + * 'parameters' => [ + * 'userId' => 1 + * ] + * ] + * ); + * ``` * @param string $sql The query string to execute. - * @param array $options [optional] Configuration options. + * @param array $options [optional] { + * Configuration options. + * + * @type array $parameters A key/value array of Query Parameters, where + * the key is represented in the query string prefixed by a `@` + * symbol. + * } * @return Result */ public function execute($sql, array $options = []) diff --git a/src/Spanner/ValueInterface.php b/src/Spanner/ValueInterface.php new file mode 100644 index 000000000000..e5e140d1c751 --- /dev/null +++ b/src/Spanner/ValueInterface.php @@ -0,0 +1,44 @@ +returnInt64AsObject = $returnInt64AsObject; + } + + /** + * Accepts an array of key/value pairs, where the key is a SQL parameter + * name and the value is the value interpolated by the server, and returns + * an array of parameters and inferred parameter types. + * + * @param array $parameters The key/value parameters. + * @return array An associative array containing params and paramTypes. + */ + public function formatParamsForExecuteSql(array $parameters) + { + $paramTypes = []; + + foreach ($parameters as $key => $value) { + list ($parameters[$key], $paramTypes[$key]) = $this->paramType($value); + } + + return [ + 'params' => $parameters, + 'paramTypes' => $paramTypes + ]; + } + + /** + * Accepts a list of values and encodes the value into a format accepted by + * the Spanner API. + * + * @param array $values The list of values + * @return array The encoded values + */ + public function encodeValuesAsSimpleType(array $values) + { + $res = []; + + foreach ($values as $value) { + if ($value instanceof ValueInterface) { + $value = $value->formatAsString(); + } + + if (gettype($value) === 'integer') { + $value = (string) $value; + } + + if ($value instanceof Int64) { + $value = $value->get(); + } + + $res[] = $value; + } + + return $res; + } + + /** + * Accepts a list of columns (with name and type) and a row from read or + * executeSql and decodes each value to its corresponding PHP type. + * + * @param array $columns The list of columns + * @param array $row The row data. + * @return array The decoded row data. + */ + public function decodeValues(array $columns, array $row) + { + $cols = []; + $types = []; + foreach (array_keys($row) as $colIndex) { + $cols[] = $columns[$colIndex]['name']; + $types[] = $columns[$colIndex]['type']['code']; + } + + $res = []; + foreach ($row as $index => $value) { + $res[$cols[$index]] = $this->decodeValue($value, $types[$index]); + } + + return $res; + } + + private function decodeValue($value, $type) + { + switch ($type) { + case self::TYPE_INT64: + $value = $this->returnInt64AsObject + ? new Int64($value) + : (int) $value; + break; + + case self::TYPE_TIMESTAMP: + $matches = []; + preg_match(self::NANO_REGEX, $value, $matches); + $value = preg_replace(self::NANO_REGEX, '.0Z', $value); + + $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $value); + $value = new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0); + break; + + case self::TYPE_DATE: + $value = new Date(new \DateTimeImmutable($value)); + break; + + case self::TYPE_BYTES: + $value = new Bytes(base64_decode($value)); + break; + + case self::TYPE_ARRAY: + $value = ''; + break; + + case self::TYPE_STRUCT: + $value = ''; + break; + } + + return $value; + } + + private function paramType($value) + { + $phpType = gettype($value); + switch ($phpType) { + case 'boolean': + $type = $this->typeObject(self::TYPE_BOOL); + break; + + case 'integer': + $value = (string) $value; + $type = $this->typeObject(self::TYPE_INT64); + break; + + case 'double': + $type = $this->typeObject(self::TYPE_FLOAT64); + break; + + case 'string': + $type = $this->typeObject(self::TYPE_STRING); + break; + + case 'resource': + $type = $this->typeObject(self::TYPE_BYTES); + $value = base64_encode(stream_get_contents($value)); + break; + + case 'object': + list ($type, $value) = $this->objectParam($value); + break; + + case 'array': + list ($type, $value) = $this->arrayOrStructObject($value); + break; + + case 'NULL': + $type = null; + break; + + default: + throw new \InvalidArgumentException(sprintf( + 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.', + $phpType + )); + break; + } + + return [$value, $type]; + } + + private function objectParam($value) + { + if ($value instanceof \stdClass) { + return $this->arrayOrStructObject($value); + } + + if ($value instanceof ValueInterface) { + return [ + $this->typeObject($value->type()), + $value->formatAsString() + ]; + } + + throw new \InvalidArgumentException(sprintf( + 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.', + get_class($value) + )); + } + + private function arrayOrStructObject(array $arrayOrStruct) + { + $type = null; + $nestedTypeKey = 'fake'; + $nestedTypeData = []; + + if ($arrayOrStruct instanceof \stdClass) { + $type = self::TYPE_STRUCT; + $nestedTypeKey = 'structType'; + $nestedTypeData = []; + } elseif ($this->isAssoc($arrayOrStruct)) { + $type = self::TYPE_STRUCT; + $nestedTypeKey = 'structType'; + $nestedTypeData = []; + } else { + $type = self::TYPE_ARRAY; + $nestedTypeKey = 'arrayElementType'; + + foreach ($arrayOrStruct as $element) { + $nestedTypeData[] = $this->paramType($element)[1]; + } + } + + return [ + $this->typeObject($type) + [ + $nestedTypeKey = $nestedTypeData + ], + $nestedTypeData + ]; + } + + private function typeObject($type) + { + return [ + 'code' => $type + ]; + } +} From 1edba828c889ff7b31b68671b9b0127485a9ab94 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Sat, 31 Dec 2016 14:45:13 -0500 Subject: [PATCH 020/107] Handle Arrays, Structs and NaN, INF, -INF correctly --- src/Spanner/Database.php | 8 +-- src/Spanner/ValueMapper.php | 98 ++++++++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index a6eee0caba46..245d6191ba12 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -196,14 +196,14 @@ public function updateDdl($statement, array $options = []) * $database->updateDdlBatch([ * 'CREATE TABLE Users ( * id INT64 NOT NULL, - * name STRING(100) NOT NULL + * name STRING(100) NOT NULL, * password STRING(100) NOT NULL - * )', + * ) PRIMARY KEY (id)', * 'CREATE TABLE Posts ( * id INT64 NOT NULL, - * title STRING(100) NOT NULL + * title STRING(100) NOT NULL, * content STRING(MAX) NOT NULL - * )' + * ) PRIMARY KEY(id)' * ]); * ``` * diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index ebe46bf4fd3f..e236108bd7bc 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -82,19 +82,7 @@ public function encodeValuesAsSimpleType(array $values) $res = []; foreach ($values as $value) { - if ($value instanceof ValueInterface) { - $value = $value->formatAsString(); - } - - if (gettype($value) === 'integer') { - $value = (string) $value; - } - - if ($value instanceof Int64) { - $value = $value->get(); - } - - $res[] = $value; + $res[] = $this->encodeValue($value); } return $res; @@ -114,7 +102,7 @@ public function decodeValues(array $columns, array $row) $types = []; foreach (array_keys($row) as $colIndex) { $cols[] = $columns[$colIndex]['name']; - $types[] = $columns[$colIndex]['type']['code']; + $types[] = $columns[$colIndex]['type']; } $res = []; @@ -125,9 +113,35 @@ public function decodeValues(array $columns, array $row) return $res; } - private function decodeValue($value, $type) + private function encodeValue($value) { - switch ($type) { + if ($value instanceof ValueInterface) { + $value = $value->formatAsString(); + } + + if ($value instanceof Int64) { + $value = $value->get(); + } + + if (gettype($value) === 'integer') { + $value = (string) $value; + } + + if (is_array($value)) { + $res = []; + foreach ($value as $item) { + $res[] = $this->encodeValue($item); + } + + $value = $res; + } + + return $value; + } + + private function decodeValue($value, array $type) + { + switch ($type['code']) { case self::TYPE_INT64: $value = $this->returnInt64AsObject ? new Int64($value) @@ -152,17 +166,65 @@ private function decodeValue($value, $type) break; case self::TYPE_ARRAY: - $value = ''; + $res = []; + foreach ($value as $item) { + $res[] = $this->decodeValue($item, $type['arrayElementType']); + } + + $value = $res; break; case self::TYPE_STRUCT: - $value = ''; + $res = []; + + foreach ($value as $index => $item) { + $res[] = $this->decodeValue($item, $type['structType']['fields'][$index]); + } + + $value = $res; + break; + + case self::TYPE_FLOAT64: + + // NaN, Infinite and -Infinite are possible FLOAT64 values, + // but when the gRPC response is decoded, they are represented + // as strings. This conditional checks for a string, converts to + // an equivalent double value, or dies if something really weird + // happens. + if (is_string($value)) { + switch ($value) { + case 'NaN': + $value = NAN; + break; + + case 'Infinity': + $value = INF; + break; + + case '-Infinity': + $value = -INF; + break; + + default: + throw new \InvalidArgumentException(sprintf( + 'Unexpected string value %s encountered in FLOAT64 field.', + $value + )); + } + } + break; } return $value; } + /** + * Create a spanner parameter type value object from a PHP value type. + * + * @param mixed $value The PHP value + * @return array The Value type + */ private function paramType($value) { $phpType = gettype($value); From 85dc60e4e8f88e7e130bc5266625268b4161de53 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 2 Jan 2017 09:55:19 -0500 Subject: [PATCH 021/107] Fix existing tests --- src/Spanner/SpannerClient.php | 30 ++--- .../ConfigurationTest.php | 2 +- .../Admin => SpannerAdmin}/DatabaseTest.php | 2 +- .../Admin => SpannerAdmin}/InstanceTest.php | 2 +- .../SpannerClientTest.php | 2 +- tests/unit/Spanner/SpannerClientTest.php | 105 ++++++++++++++++++ .../ConfigurationTest.php | 2 +- .../Connection/GrpcTest.php | 2 +- .../Connection/IamDatabaseTest.php | 2 +- .../Connection/IamInstanceTest.php | 2 +- .../Admin => SpannerAdmin}/DatabaseTest.php | 2 +- .../Admin => SpannerAdmin}/InstanceTest.php | 6 +- .../SpannerClientTest.php | 2 +- 13 files changed, 133 insertions(+), 28 deletions(-) rename tests/snippets/{Spanner/Admin => SpannerAdmin}/ConfigurationTest.php (98%) rename tests/snippets/{Spanner/Admin => SpannerAdmin}/DatabaseTest.php (98%) rename tests/snippets/{Spanner/Admin => SpannerAdmin}/InstanceTest.php (99%) rename tests/snippets/{Spanner/Admin => SpannerAdmin}/SpannerClientTest.php (98%) create mode 100644 tests/unit/Spanner/SpannerClientTest.php rename tests/unit/{Spanner/Admin => SpannerAdmin}/ConfigurationTest.php (98%) rename tests/unit/{Spanner/Admin => SpannerAdmin}/Connection/GrpcTest.php (98%) rename tests/unit/{Spanner/Admin => SpannerAdmin}/Connection/IamDatabaseTest.php (97%) rename tests/unit/{Spanner/Admin => SpannerAdmin}/Connection/IamInstanceTest.php (97%) rename tests/unit/{Spanner/Admin => SpannerAdmin}/DatabaseTest.php (99%) rename tests/unit/{Spanner/Admin => SpannerAdmin}/InstanceTest.php (97%) rename tests/unit/{Spanner/Admin => SpannerAdmin}/SpannerClientTest.php (99%) diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 6383df85f2ef..aa2e8c4a2e3b 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -334,21 +334,6 @@ public function keyRange(array $range = []) return new KeyRange($range); } - /** - * Get the session client - * - * Example: - * ``` - * $sessionClient = $spanner->sessionClient(); - * ``` - * - * @return SessionClient - */ - public function sessionClient() - { - return $this->sessionClient; - } - /** * Create a Bytes object. * @@ -414,4 +399,19 @@ public function int64($value) { return new Int64($value); } + + /** + * Get the session client + * + * Example: + * ``` + * $sessionClient = $spanner->sessionClient(); + * ``` + * + * @return SessionClient + */ + public function sessionClient() + { + return $this->sessionClient; + } } diff --git a/tests/snippets/Spanner/Admin/ConfigurationTest.php b/tests/snippets/SpannerAdmin/ConfigurationTest.php similarity index 98% rename from tests/snippets/Spanner/Admin/ConfigurationTest.php rename to tests/snippets/SpannerAdmin/ConfigurationTest.php index 530ddac3898b..02ea3e29ac90 100644 --- a/tests/snippets/Spanner/Admin/ConfigurationTest.php +++ b/tests/snippets/SpannerAdmin/ConfigurationTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Snippets\Spanner\Admin; +namespace Google\Cloud\Tests\Snippets\SpannerAdmin; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Spanner\Configuration; diff --git a/tests/snippets/Spanner/Admin/DatabaseTest.php b/tests/snippets/SpannerAdmin/DatabaseTest.php similarity index 98% rename from tests/snippets/Spanner/Admin/DatabaseTest.php rename to tests/snippets/SpannerAdmin/DatabaseTest.php index 5ed75e6d988b..d0252adb1382 100644 --- a/tests/snippets/Spanner/Admin/DatabaseTest.php +++ b/tests/snippets/SpannerAdmin/DatabaseTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Snippets\Spanner\Admin; +namespace Google\Cloud\Tests\Snippets\SpannerAdmin; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Iam\Iam; diff --git a/tests/snippets/Spanner/Admin/InstanceTest.php b/tests/snippets/SpannerAdmin/InstanceTest.php similarity index 99% rename from tests/snippets/Spanner/Admin/InstanceTest.php rename to tests/snippets/SpannerAdmin/InstanceTest.php index e56274946b6f..46fee5820b07 100644 --- a/tests/snippets/Spanner/Admin/InstanceTest.php +++ b/tests/snippets/SpannerAdmin/InstanceTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Snippets\Spanner\Admin; +namespace Google\Cloud\Tests\Snippets\SpannerAdmin; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Iam\Iam; diff --git a/tests/snippets/Spanner/Admin/SpannerClientTest.php b/tests/snippets/SpannerAdmin/SpannerClientTest.php similarity index 98% rename from tests/snippets/Spanner/Admin/SpannerClientTest.php rename to tests/snippets/SpannerAdmin/SpannerClientTest.php index b4e258c6e250..410935587e75 100644 --- a/tests/snippets/Spanner/Admin/SpannerClientTest.php +++ b/tests/snippets/SpannerAdmin/SpannerClientTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Snippets\Spanner\Admin; +namespace Google\Cloud\Tests\Snippets\SpannerAdmin; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Spanner\Configuration; diff --git a/tests/unit/Spanner/SpannerClientTest.php b/tests/unit/Spanner/SpannerClientTest.php new file mode 100644 index 000000000000..17bf7d6406db --- /dev/null +++ b/tests/unit/Spanner/SpannerClientTest.php @@ -0,0 +1,105 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->client = \Google\Cloud\Dev\stub(SpannerClient::class); + } + + public function testConnect() + { + $database = $this->client->connect(self::INSTANCE, self::DATABASE); + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals(self::DATABASE, $database->name()); + } + + public function testConnectWithInstance() + { + $inst = $this->client->instance(self::INSTANCE); + $database = $this->client->connect($inst, self::DATABASE); + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals(self::DATABASE, $database->name()); + } + + public function testKeyset() + { + $ks = $this->client->keySet(); + $this->assertInstanceOf(KeySet::class, $ks); + } + + public function testKeyRange() + { + $kr = $this->client->keyRange(); + $this->assertInstanceOf(KeyRange::class, $kr); + } + + public function testBytes() + { + $b = $this->client->bytes('foo'); + $this->assertInstanceOf(Bytes::class, $b); + $this->assertEquals(base64_encode('foo'), (string)$b); + } + + public function testDate() + { + $d = $this->client->date(new \DateTime); + $this->assertInstanceOf(Date::class, $d); + } + + public function testTimestamp() + { + $ts = $this->client->timestamp(new \DateTime); + $this->assertInstanceOf(Timestamp::class, $ts); + } + + public function testInt64() + { + $i64 = $this->client->int64('123'); + $this->assertInstanceOf(Int64::class, $i64); + } + + public function testSessionClient() + { + $sc = $this->client->sessionClient(); + $this->assertInstanceOf(SessionClient::class, $sc); + } +} diff --git a/tests/unit/Spanner/Admin/ConfigurationTest.php b/tests/unit/SpannerAdmin/ConfigurationTest.php similarity index 98% rename from tests/unit/Spanner/Admin/ConfigurationTest.php rename to tests/unit/SpannerAdmin/ConfigurationTest.php index 1edd032654af..9ad7460446cc 100644 --- a/tests/unit/Spanner/Admin/ConfigurationTest.php +++ b/tests/unit/SpannerAdmin/ConfigurationTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Unit\Spanner\Admin; +namespace Google\Cloud\Tests\Unit\SpannerAdmin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; diff --git a/tests/unit/Spanner/Admin/Connection/GrpcTest.php b/tests/unit/SpannerAdmin/Connection/GrpcTest.php similarity index 98% rename from tests/unit/Spanner/Admin/Connection/GrpcTest.php rename to tests/unit/SpannerAdmin/Connection/GrpcTest.php index ebfe45f19e6e..1eb2341e3b25 100644 --- a/tests/unit/Spanner/Admin/Connection/GrpcTest.php +++ b/tests/unit/SpannerAdmin/Connection/GrpcTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Unit\Spanner\Admin\Connection; +namespace Google\Cloud\Tests\Unit\SpannerAdmin\Connection; use Google\Cloud\GrpcRequestWrapper; use Google\Cloud\GrpcTrait; diff --git a/tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php b/tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php similarity index 97% rename from tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php rename to tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php index aaf664150aad..edacd45a30e9 100644 --- a/tests/unit/Spanner/Admin/Connection/IamDatabaseTest.php +++ b/tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Unit\Spanner\Admin\Connection; +namespace Google\Cloud\Tests\Unit\SpannerAdmin\Connection; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; diff --git a/tests/unit/Spanner/Admin/Connection/IamInstanceTest.php b/tests/unit/SpannerAdmin/Connection/IamInstanceTest.php similarity index 97% rename from tests/unit/Spanner/Admin/Connection/IamInstanceTest.php rename to tests/unit/SpannerAdmin/Connection/IamInstanceTest.php index 7bb4377797f9..6463a895071d 100644 --- a/tests/unit/Spanner/Admin/Connection/IamInstanceTest.php +++ b/tests/unit/SpannerAdmin/Connection/IamInstanceTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Unit\Spanner\Admin\Connection; +namespace Google\Cloud\Tests\Unit\SpannerAdmin\Connection; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamInstance; diff --git a/tests/unit/Spanner/Admin/DatabaseTest.php b/tests/unit/SpannerAdmin/DatabaseTest.php similarity index 99% rename from tests/unit/Spanner/Admin/DatabaseTest.php rename to tests/unit/SpannerAdmin/DatabaseTest.php index 6407ab905573..ff205ad0c52a 100644 --- a/tests/unit/Spanner/Admin/DatabaseTest.php +++ b/tests/unit/SpannerAdmin/DatabaseTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Unit\Spanner\Admin; +namespace Google\Cloud\Tests\Unit\SpannerAdmin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; diff --git a/tests/unit/Spanner/Admin/InstanceTest.php b/tests/unit/SpannerAdmin/InstanceTest.php similarity index 97% rename from tests/unit/Spanner/Admin/InstanceTest.php rename to tests/unit/SpannerAdmin/InstanceTest.php index b8e600942ece..960101d87e4d 100644 --- a/tests/unit/Spanner/Admin/InstanceTest.php +++ b/tests/unit/SpannerAdmin/InstanceTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Unit\Spanner\Admin; +namespace Google\Cloud\Tests\Unit\SpannerAdmin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; @@ -59,7 +59,7 @@ public function testInfo() { $this->connection->getInstance()->shouldNotBeCalled(); - $instance = new Instance($this->connection->reveal(), $this->prophesize(SessionPoolInterface::class)->reveal(), self::PROJECT_ID, self::NAME, ['foo' => 'bar']); + $instance = new Instance($this->connection->reveal(), $this->prophesize(SessionPoolInterface::class)->reveal(), self::PROJECT_ID, self::NAME, false, ['foo' => 'bar']); $this->assertEquals('bar', $instance->info()['foo']); } @@ -284,6 +284,6 @@ public function testIam() private function getDefaultInstance() { - return json_decode(file_get_contents(__DIR__ .'/../../fixtures/spanner/instance.json'), true); + return json_decode(file_get_contents(__DIR__ .'/../fixtures/spanner/instance.json'), true); } } diff --git a/tests/unit/Spanner/Admin/SpannerClientTest.php b/tests/unit/SpannerAdmin/SpannerClientTest.php similarity index 99% rename from tests/unit/Spanner/Admin/SpannerClientTest.php rename to tests/unit/SpannerAdmin/SpannerClientTest.php index 4d38098f9e38..493d3f7b1fe1 100644 --- a/tests/unit/Spanner/Admin/SpannerClientTest.php +++ b/tests/unit/SpannerAdmin/SpannerClientTest.php @@ -15,7 +15,7 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Unit\Spanner\Admin; +namespace Google\Cloud\Tests\Unit\SpannerAdmin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Spanner\Configuration; From 0838cb9efb5a617f337727770b0d469865293684 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 2 Jan 2017 12:53:42 -0500 Subject: [PATCH 022/107] Value Type Tests --- src/Spanner/ValueMapper.php | 2 +- tests/unit/Spanner/BytesTest.php | 52 ++++++++++++++++++++++++ tests/unit/Spanner/DateTest.php | 55 +++++++++++++++++++++++++ tests/unit/Spanner/TimestampTest.php | 61 ++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 tests/unit/Spanner/BytesTest.php create mode 100644 tests/unit/Spanner/DateTest.php create mode 100644 tests/unit/Spanner/TimestampTest.php diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index e236108bd7bc..a5f1e32c29ec 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -296,7 +296,7 @@ private function objectParam($value) private function arrayOrStructObject(array $arrayOrStruct) { $type = null; - $nestedTypeKey = 'fake'; + $nestedTypeKey = ''; $nestedTypeData = []; if ($arrayOrStruct instanceof \stdClass) { diff --git a/tests/unit/Spanner/BytesTest.php b/tests/unit/Spanner/BytesTest.php new file mode 100644 index 000000000000..d224738f96e2 --- /dev/null +++ b/tests/unit/Spanner/BytesTest.php @@ -0,0 +1,52 @@ +content); + $this->assertEquals($this->content, $bytes->get()); + } + + public function testFormatAsString() + { + $bytes = new Bytes($this->content); + $this->assertEquals(base64_encode($this->content), $bytes->formatAsString()); + } + + public function testCast() + { + $bytes = new Bytes($this->content); + $this->assertEquals(base64_encode($this->content), (string) $bytes); + } + + public function testType() + { + $bytes = new Bytes($this->content); + $this->assertTrue(is_integer($bytes->type())); + } +} diff --git a/tests/unit/Spanner/DateTest.php b/tests/unit/Spanner/DateTest.php new file mode 100644 index 000000000000..1c25c6bc3245 --- /dev/null +++ b/tests/unit/Spanner/DateTest.php @@ -0,0 +1,55 @@ +dt = new \DateTime('1989-10-11'); + $this->date = new Date($this->dt); + } + + public function testGet() + { + $this->assertEquals($this->dt, $this->date->get()); + } + + public function testFormatAsString() + { + $this->assertEquals($this->dt->format(Date::FORMAT), $this->date->formatAsString()); + } + + public function testCast() + { + $this->assertEquals($this->dt->format(Date::FORMAT), (string)$this->date); + } + + public function testType() + { + $this->assertTrue(is_integer($this->date->type())); + } +} diff --git a/tests/unit/Spanner/TimestampTest.php b/tests/unit/Spanner/TimestampTest.php new file mode 100644 index 000000000000..c18ee94e95c2 --- /dev/null +++ b/tests/unit/Spanner/TimestampTest.php @@ -0,0 +1,61 @@ +dt = new \DateTime('1989-10-11 08:58:00 +00:00'); + $this->ts = new Timestamp($this->dt); + } + + public function testGet() + { + $this->assertEquals($this->dt, $this->ts->get()); + } + + public function testFormatAsString() + { + $this->assertEquals( + (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'), + (new \DateTime($this->ts->formatAsString()))->format('U') + ); + } + + public function testCast() + { + $this->assertEquals( + (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'), + (new \DateTime((string)$this->ts))->format('U') + ); + } + + public function testType() + { + $this->assertTrue(is_integer($this->ts->type())); + } +} From 0d738b3c95089dc932aea8727aebcc05159f5d21 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 2 Jan 2017 12:58:39 -0500 Subject: [PATCH 023/107] Address code review --- src/Spanner/Database.php | 2 +- src/Spanner/ValueMapper.php | 44 +++++++------------------------------ 2 files changed, 9 insertions(+), 37 deletions(-) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 245d6191ba12..5476ccecc186 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -360,7 +360,7 @@ public function insertBatch($table, array $dataSet, array $options = []) foreach ($dataSet as $data) { $mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data); } -// print_R($mutations);exit; + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); return $this->operation->commit($session, $mutations, $options); diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index e236108bd7bc..557271b37b5a 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -256,7 +256,14 @@ private function paramType($value) break; case 'array': - list ($type, $value) = $this->arrayOrStructObject($value); + $type = $this->typeObject(self::TYPE_ARRAY); + + $res = []; + foreach ($value as $element) { + $res[] = $this->paramType($element)[1]; + } + + $value = $res; break; case 'NULL': @@ -276,10 +283,6 @@ private function paramType($value) private function objectParam($value) { - if ($value instanceof \stdClass) { - return $this->arrayOrStructObject($value); - } - if ($value instanceof ValueInterface) { return [ $this->typeObject($value->type()), @@ -293,37 +296,6 @@ private function objectParam($value) )); } - private function arrayOrStructObject(array $arrayOrStruct) - { - $type = null; - $nestedTypeKey = 'fake'; - $nestedTypeData = []; - - if ($arrayOrStruct instanceof \stdClass) { - $type = self::TYPE_STRUCT; - $nestedTypeKey = 'structType'; - $nestedTypeData = []; - } elseif ($this->isAssoc($arrayOrStruct)) { - $type = self::TYPE_STRUCT; - $nestedTypeKey = 'structType'; - $nestedTypeData = []; - } else { - $type = self::TYPE_ARRAY; - $nestedTypeKey = 'arrayElementType'; - - foreach ($arrayOrStruct as $element) { - $nestedTypeData[] = $this->paramType($element)[1]; - } - } - - return [ - $this->typeObject($type) + [ - $nestedTypeKey = $nestedTypeData - ], - $nestedTypeData - ]; - } - private function typeObject($type) { return [ From 364be9758fa25321dabde6e9f27f5421285ec1b5 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Tue, 3 Jan 2017 10:58:31 -0500 Subject: [PATCH 024/107] Finish covering the easy stuff --- src/Spanner/Result.php | 2 +- tests/unit/Spanner/KeyRangeTest.php | 64 +++++++++++++++++++++ tests/unit/Spanner/KeySetTest.php | 89 +++++++++++++++++++++++++++++ tests/unit/Spanner/ResultTest.php | 68 ++++++++++++++++++++++ 4 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 tests/unit/Spanner/KeyRangeTest.php create mode 100644 tests/unit/Spanner/KeySetTest.php create mode 100644 tests/unit/Spanner/ResultTest.php diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index e813864b7a08..ca32428c77dc 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -75,7 +75,7 @@ public function rows() public function stats() { return (isset($this->result['stats'])) - ? $result['stats'] + ? $this->result['stats'] : null; } diff --git a/tests/unit/Spanner/KeyRangeTest.php b/tests/unit/Spanner/KeyRangeTest.php new file mode 100644 index 000000000000..9b50d5259f9b --- /dev/null +++ b/tests/unit/Spanner/KeyRangeTest.php @@ -0,0 +1,64 @@ +setStartOpen($this->startOpen); + $kr->setStartClosed($this->startClosed); + $kr->setEndOpen($this->endOpen); + $kr->setEndClosed($this->endClosed); + + $this->assertThings($kr); + } + + public function testConstructValues() + { + $kr = new KeyRange([ + 'startOpen' => $this->startOpen, + 'startClosed' => $this->startClosed, + 'endOpen' => $this->endOpen, + 'endClosed' => $this->endClosed + ]); + + $this->assertThings($kr); + } + + private function assertThings($kr) + { + $this->assertEquals([ + 'startOpen' => $this->startOpen, + 'startClosed' => $this->startClosed, + 'endOpen' => $this->endOpen, + 'endClosed' => $this->endClosed + ], $kr->keyRangeObject()); + } +} diff --git a/tests/unit/Spanner/KeySetTest.php b/tests/unit/Spanner/KeySetTest.php new file mode 100644 index 000000000000..be4efcb52cb1 --- /dev/null +++ b/tests/unit/Spanner/KeySetTest.php @@ -0,0 +1,89 @@ + 'foo']); + + $set->addRange($range); + + $this->assertEquals($range->keyRangeObject(), $set->keySetObject()['ranges'][0]); + } + + public function testSetRanges() + { + $set = new KeySet; + + $ranges = [ + new KeyRange(['setStartOpen' => 'foo']), + new KeyRange(['setStartOpen' => 'bar']), + ]; + + $set->setRanges($ranges); + + $expected = []; + foreach ($ranges as $r) { + $expected[] = $r->keyRangeObject(); + } + + $this->assertEquals($expected, $set->keySetObject()['ranges']); + } + + public function testAddKey() + { + $set = new KeySet; + + $key = 'key'; + + $set->addKey($key); + + $this->assertEquals($key, $set->keySetObject()['keys'][0]); + } + + public function testSetKeys() + { + $set = new KeySet; + + $keys = ['key1','key2']; + + $set->setKeys($keys); + + $this->assertEquals($keys, $set->keySetObject()['keys']); + } + + public function testSetAll() + { + $set = new KeySet; + + $set->setAll(true); + $this->assertTrue($set->keySetObject()['all']); + + $set->setAll(false); + $this->assertFalse($set->keySetObject()['all']); + } +} diff --git a/tests/unit/Spanner/ResultTest.php b/tests/unit/Spanner/ResultTest.php new file mode 100644 index 000000000000..e5fa12d6c38a --- /dev/null +++ b/tests/unit/Spanner/ResultTest.php @@ -0,0 +1,68 @@ + 'John'] + ]); + + $res = iterator_to_array($result); + $this->assertEquals(1, count($res)); + $this->assertEquals('John', $res[0]['name']); + } + + public function testMetadata() + { + $result = new Result(['metadata' => 'foo'], []); + $this->assertEquals('foo', $result->metadata()); + } + + public function testRows() + { + $rows = [ + ['name' => 'John'] + ]; + + $result = new Result([], $rows); + + $this->assertEquals($rows, $result->rows()); + } + + public function testStats() + { + $result = new Result(['stats' => 'foo'], []); + $this->assertEquals('foo', $result->stats()); + } + + public function testInfo() + { + $info = ['foo' => 'bar']; + $result = new Result($info, []); + + $this->assertEquals($info, $result->info()); + } +} From 518bbdb6077f94606455efb196c0bd9b2f7a7baf Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Thu, 5 Jan 2017 15:04:20 -0500 Subject: [PATCH 025/107] Cover ValueMapper --- src/Spanner/Timestamp.php | 5 +- src/Spanner/ValueMapper.php | 98 +++---- tests/unit/Spanner/ValueMapperTest.php | 375 +++++++++++++++++++++++++ 3 files changed, 428 insertions(+), 50 deletions(-) diff --git a/src/Spanner/Timestamp.php b/src/Spanner/Timestamp.php index 56884dc5551f..de27388f272d 100644 --- a/src/Spanner/Timestamp.php +++ b/src/Spanner/Timestamp.php @@ -34,7 +34,7 @@ class Timestamp implements ValueInterface { const FORMAT = 'Y-m-d\TH:i:s.u\Z'; - const FORMAT_INTERPOLATE = 'Y-m-d\TH:i:s.%\d\Z'; + const FORMAT_INTERPOLATE = 'Y-m-d\TH:i:s.%\s\Z'; /** * @var \DateTimeInterface @@ -84,7 +84,8 @@ public function type() public function formatAsString() { $this->value->setTimezone(new \DateTimeZone('UTC')); - return sprintf($this->value->format(self::FORMAT_INTERPOLATE), $this->nanoSeconds); + $ns = str_pad((string) $this->nanoSeconds, 6, '0', STR_PAD_LEFT); + return sprintf($this->value->format(self::FORMAT_INTERPOLATE), $ns); } /** diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index 557271b37b5a..0887deb358be 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -80,9 +80,8 @@ public function formatParamsForExecuteSql(array $parameters) public function encodeValuesAsSimpleType(array $values) { $res = []; - foreach ($values as $value) { - $res[] = $this->encodeValue($value); + $res[] = $this->paramType($value)[0]; } return $res; @@ -96,49 +95,34 @@ public function encodeValuesAsSimpleType(array $values) * @param array $row The row data. * @return array The decoded row data. */ - public function decodeValues(array $columns, array $row) + public function decodeValues(array $columns, array $row, $extractResult = false) { $cols = []; $types = []; - foreach (array_keys($row) as $colIndex) { - $cols[] = $columns[$colIndex]['name']; - $types[] = $columns[$colIndex]['type']; + + foreach ($columns as $index => $column) { + $cols[] = (isset($column['name'])) + ? $column['name'] + : $index; + $types[] = $column['type']; } $res = []; foreach ($row as $index => $value) { - $res[$cols[$index]] = $this->decodeValue($value, $types[$index]); + $i = $cols[$index]; + $res[$i] = $this->decodeValue($value, $types[$index]); } return $res; } - private function encodeValue($value) - { - if ($value instanceof ValueInterface) { - $value = $value->formatAsString(); - } - - if ($value instanceof Int64) { - $value = $value->get(); - } - - if (gettype($value) === 'integer') { - $value = (string) $value; - } - - if (is_array($value)) { - $res = []; - foreach ($value as $item) { - $res[] = $this->encodeValue($item); - } - - $value = $res; - } - - return $value; - } - + /** + * Convert a single value to its corresponding PHP type. + * + * @param mixed $value The value to decode + * @param array $type The value type + * @return mixed + */ private function decodeValue($value, array $type) { switch ($type['code']) { @@ -151,7 +135,7 @@ private function decodeValue($value, array $type) case self::TYPE_TIMESTAMP: $matches = []; preg_match(self::NANO_REGEX, $value, $matches); - $value = preg_replace(self::NANO_REGEX, '.0Z', $value); + $value = preg_replace(self::NANO_REGEX, '.000000Z', $value); $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $value); $value = new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0); @@ -175,13 +159,7 @@ private function decodeValue($value, array $type) break; case self::TYPE_STRUCT: - $res = []; - - foreach ($value as $index => $item) { - $res[] = $this->decodeValue($item, $type['structType']['fields'][$index]); - } - - $value = $res; + $value = $this->decodeValues($type['structType']['fields'], $value, true); break; case self::TYPE_FLOAT64: @@ -206,7 +184,7 @@ private function decodeValue($value, array $type) break; default: - throw new \InvalidArgumentException(sprintf( + throw new \RuntimeException(sprintf( 'Unexpected string value %s encountered in FLOAT64 field.', $value )); @@ -256,13 +234,29 @@ private function paramType($value) break; case 'array': - $type = $this->typeObject(self::TYPE_ARRAY); + + if ($this->isAssoc($value)) { + throw new \InvalidArgumentException('Associative arrays are not supported'); + } $res = []; + $types = []; foreach ($value as $element) { - $res[] = $this->paramType($element)[1]; + $type = $this->paramType($element); + $res[] = $type[0]; + $types[] = $type[1]['code']; + } + + if (count(array_unique($types)) !== 1) { + throw new \InvalidArgumentException('Array values may not be of mixed type'); } + $type = $this->typeObject( + self::TYPE_ARRAY, + $this->typeObject($types[0]), + 'arrayElementType' + ); + $value = $res; break; @@ -290,16 +284,24 @@ private function objectParam($value) ]; } + if ($value instanceof Int64) { + return [ + $this->typeObject(self::TYPE_INT64), + $value->get() + ]; + } + throw new \InvalidArgumentException(sprintf( 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.', get_class($value) )); } - private function typeObject($type) + private function typeObject($type, array $nestedDefinition = [], $nestedDefinitionType = null) { - return [ - 'code' => $type - ]; + return array_filter([ + 'code' => $type, + $nestedDefinitionType => $nestedDefinition + ]); } } diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php index e69de29bb2d1..8d91e010d2a3 100644 --- a/tests/unit/Spanner/ValueMapperTest.php +++ b/tests/unit/Spanner/ValueMapperTest.php @@ -0,0 +1,375 @@ +mapper = new ValueMapper(false); + } + + public function testFormatParamsForExecuteSqlSimpleTypes() + { + $params = [ + 'id' => 1, + 'name' => 'john', + 'pi' => 3.1515, + 'isCool' => false, + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($params, $res['params']); + $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['id']['code']); + $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['name']['code']); + $this->assertEquals(ValueMapper::TYPE_FLOAT64, $res['paramTypes']['pi']['code']); + $this->assertEquals(ValueMapper::TYPE_BOOL, $res['paramTypes']['isCool']['code']); + } + + public function testFormatParamsForExecuteSqlResource() + { + $c = 'hello world'; + + $resource = fopen('php://temp', 'r+'); + fwrite($resource, $c); + rewind($resource); + + $params = [ + 'resource' => $resource + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($c, base64_decode($res['params']['resource'])); + $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['resource']['code']); + } + + public function testFormatParamsForExecuteSqlArray() + { + $params = [ + 'array' => ['foo', 'bar'] + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals('foo', $res['params']['array'][0]); + $this->assertEquals('bar', $res['params']['array'][1]); + $this->assertEquals(ValueMapper::TYPE_ARRAY, $res['paramTypes']['array']['code']); + $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['array']['arrayElementType']['code']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlInvalidTypes() + { + $this->mapper->formatParamsForExecuteSql(['array' => ['foo', 3.1515]]); + } + + public function testFormatParamsForExecuteSqlInt64() + { + $val = '1234'; + $params = [ + 'int' => new Int64($val) + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($val, $res['params']['int']); + $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['int']['code']); + } + + public function testFormatParamsForExecuteSqlValueInterface() + { + $val = 'hello world'; + $params = [ + 'bytes' => new Bytes($val) + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + $this->assertEquals($val, base64_decode($res['params']['bytes'])); + $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['bytes']['code']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlInvalidObjectType() + { + $params = [ + 'bad' => $this + ]; + + $this->mapper->formatParamsForExecuteSql($params); + } + + public function testEncodeValuesAsSimpleType() + { + $dt = new \DateTime; + + $vals = []; + $vals['bool'] = true; + $vals['int'] = 555555; + $vals['intObj'] = new Int64((string) $vals['int']); + $vals['float'] = 3.1415; + $vals['nan'] = NAN; + $vals['inf'] = INF; + $vals['timestamp'] = new Timestamp($dt); + $vals['date'] = new Date($dt); + $vals['string'] = 'foo'; + $vals['bytes'] = new Bytes('hello world'); + $vals['array'] = ['foo', 'bar']; + + $res = $this->mapper->encodeValuesAsSimpleType($vals); + + $this->assertTrue($res[0]); + $this->assertEquals((string) $vals['int'], $res[1]); + $this->assertEquals((string) $vals['int'], $res[2]); + $this->assertEquals($vals['float'], $res[3]); + $this->assertTrue(is_nan($res[4])); + $this->assertEquals(INF, $res[5]); + $this->assertEquals($dt->format(Timestamp::FORMAT), $res[6]); + $this->assertEquals($dt->format(Date::FORMAT), $res[7]); + $this->assertEquals($vals['string'], $res[8]); + $this->assertEquals(base64_encode('hello world'), $res[9]); + $this->assertEquals($vals['array'], $res[10]); + } + + public function testDecodeValuesBool() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_BOOL), + $this->createRow(false) + ); + $this->assertEquals(false, $res['rowName']); + } + + public function testDecodeValuesInt() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_INT64), + $this->createRow('555') + ); + $this->assertEquals(555, $res['rowName']); + } + + public function testDecodeValuesInt64Object() + { + $mapper = new ValueMapper(true); + $res = $mapper->decodeValues( + $this->createField(ValueMapper::TYPE_INT64), + $this->createRow('555') + ); + $this->assertInstanceOf(Int64::class, $res['rowName']); + $this->assertEquals('555', $res['rowName']->get()); + } + + public function testDecodeValuesFloat() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow(3.1415) + ); + $this->assertEquals(3.1415, $res['rowName']); + } + + public function testDecodeValuesFloatNaN() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('NaN') + ); + $this->assertTrue(is_nan($res['rowName'])); + } + + public function testDecodeValuesFloatInfinity() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('Infinity') + ); + + $this->assertTrue(is_infinite($res['rowName'])); + $this->assertTrue($res['rowName'] > 0); + } + + public function testDecodeValuesFloatNegativeInfinity() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('-Infinity') + ); + + $this->assertTrue(is_infinite($res['rowName'])); + $this->assertTrue($res['rowName'] < 0); + } + + /** + * @expectedException RuntimeException + */ + public function testDecodeValuesFloatError() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('foo') + ); + } + + public function testDecodeValuesString() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_STRING), + $this->createRow('foo') + ); + $this->assertEquals('foo', $res['rowName']); + } + + public function testDecodeValuesTimestamp() + { + $dt = new \DateTime; + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_TIMESTAMP), + $this->createRow($dt->format(Timestamp::FORMAT)) + ); + + $this->assertInstanceOf(Timestamp::class, $res['rowName']); + $this->assertEquals($dt->format(Timestamp::FORMAT), $res['rowName']->formatAsString()); + } + + public function testDecodeValuesDate() + { + $dt = new \DateTime; + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_DATE), + $this->createRow($dt->format(Date::FORMAT)) + ); + + $this->assertInstanceOf(Date::class, $res['rowName']); + $this->assertEquals($dt->format(Date::FORMAT), $res['rowName']->formatAsString()); + } + + public function testDecodeValuesBytes() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_BYTES), + $this->createRow(base64_encode('hello world')) + ); + + $this->assertInstanceOf(Bytes::class, $res['rowName']); + $this->assertEquals('hello world', $res['rowName']->get()); + } + + public function testDecodeValuesArray() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_ARRAY, 'arrayElementType', [ + 'code' => ValueMapper::TYPE_STRING + ]), $this->createRow(['foo', 'bar']) + ); + + $this->assertEquals('foo', $res['rowName'][0]); + $this->assertEquals('bar', $res['rowName'][1]); + } + + public function testDecodeValuesStruct() + { + $field = [ + 'name' => 'structTest', + 'type' => [ + 'code' => ValueMapper::TYPE_ARRAY, + 'arrayElementType' => [ + 'code' => ValueMapper::TYPE_STRUCT, + 'structType' => [ + 'fields' => [ + [ + 'name' => 'rowName', + 'type' => [ + 'code' => ValueMapper::TYPE_STRING + ] + ] + ] + ] + ] + ] + ]; + + $row = [ + [ + 'Hello World' + ] + ]; + + $res = $this->mapper->decodeValues( + [$field], + [$row] + ); + + $this->assertEquals('Hello World', $res['structTest'][0]['rowName']); + } + + public function testDecodeValuesAnonymousField() + { + $fields = [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64, + ] + ], [ + 'type' => [ + 'code' => ValueMapper::TYPE_STRING + ] + ] + ]; + + $row = ['1337', 'John']; + + $res = $this->mapper->decodeValues($fields, $row); + + $this->assertEquals('1337', $res['ID']); + $this->assertEquals('John', $res[1]); + } + + private function createField($code, $type = null, array $typeObj = []) + { + return [[ + 'name' => 'rowName', + 'type' => array_filter([ + 'code' => $code, + $type => $typeObj + ]) + ]]; + } + + private function createRow($val) + { + return [$val]; + } +} From 3510b78b33eeba7dae0e79ac5a79c6049cfdb879 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 6 Jan 2017 10:38:15 -0500 Subject: [PATCH 026/107] Support overloading any private property on tested class --- dev/src/Functions.php | 10 +++++-- dev/src/SetStubConnectionTrait.php | 26 ----------------- dev/src/SetStubPropertiesTrait.php | 47 ++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 29 deletions(-) delete mode 100644 dev/src/SetStubConnectionTrait.php create mode 100644 dev/src/SetStubPropertiesTrait.php diff --git a/dev/src/Functions.php b/dev/src/Functions.php index f8a36819cc90..47612d9703b1 100644 --- a/dev/src/Functions.php +++ b/dev/src/Functions.php @@ -2,14 +2,18 @@ namespace Google\Cloud\Dev; -function stub($extends, array $args = []) +function stub($extends, array $args = [], array $props = []) { - $tpl = 'class %s extends %s {use \Google\Cloud\Dev\SetStubConnectionTrait; }'; + if (empty($props)) { + $props = ['connection']; + } + + $tpl = 'class %s extends %s {private $___props = \'%s\'; use \Google\Cloud\Dev\SetStubPropertiesTrait; }'; $name = 'Stub'. sha1($extends); if (!class_exists($name)) { - eval(sprintf($tpl, $name, $extends)); + eval(sprintf($tpl, $name, $extends, json_encode($props))); } $reflection = new \ReflectionClass($name); diff --git a/dev/src/SetStubConnectionTrait.php b/dev/src/SetStubConnectionTrait.php deleted file mode 100644 index 3ffb3d5b606a..000000000000 --- a/dev/src/SetStubConnectionTrait.php +++ /dev/null @@ -1,26 +0,0 @@ -connection = $conn; - } -} diff --git a/dev/src/SetStubPropertiesTrait.php b/dev/src/SetStubPropertiesTrait.php new file mode 100644 index 000000000000..2868852b8f37 --- /dev/null +++ b/dev/src/SetStubPropertiesTrait.php @@ -0,0 +1,47 @@ +___props))) { + throw new \BadMethodCallException(sprintf('Property %s cannot be overloaded', $prop)); + } + + $trait = new \ReflectionClass($this); + $ref = $trait->getParentClass(); + + try { + $property = $ref->getProperty($prop); + } catch (\ReflectionException $e) { + throw new \BadMethodCallException($e->getMessage()); + } + + $property->setAccessible(true); + $property->setValue($this, $args[0]); + } +} From 170ce7c4563563bc5fb9307337738c3a15d2746b Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 6 Jan 2017 16:42:38 -0500 Subject: [PATCH 027/107] More unit tests, refactor KeySet and KeyRange, fix Delete --- src/ArrayTrait.php | 17 + src/Spanner/Connection/Grpc.php | 34 +- src/Spanner/Database.php | 97 +++-- src/Spanner/KeyRange.php | 184 +++++++-- src/Spanner/KeySet.php | 87 +++- src/Spanner/Operation.php | 51 ++- src/Spanner/Session/SessionPoolInterface.php | 4 +- src/Spanner/SpannerClient.php | 17 +- src/Spanner/V1/SpannerClient.php | 2 +- tests/unit/Spanner/DatabaseTest.php | 406 +++++++++++++++++++ tests/unit/Spanner/KeyRangeTest.php | 83 ++-- tests/unit/Spanner/KeySetTest.php | 58 ++- 12 files changed, 903 insertions(+), 137 deletions(-) create mode 100644 tests/unit/Spanner/DatabaseTest.php diff --git a/src/ArrayTrait.php b/src/ArrayTrait.php index 0bb5d43b1f96..eea200bf2b14 100644 --- a/src/ArrayTrait.php +++ b/src/ArrayTrait.php @@ -78,4 +78,21 @@ private function isAssoc(array $arr) { return array_keys($arr) !== range(0, count($arr) - 1); } + + /** + * Just like array_filter(), but preserves boolean values. + * + * @param array $arr + * @return array + */ + private function arrayFilterPreserveBool(array $arr) + { + return array_filter($arr, function ($element) { + if (is_bool($element)) { + return true; + } + + return $element == true; + }); + } } diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 1b7f6fff356b..54ddc9462508 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -64,7 +64,8 @@ class Grpc implements ConnectionInterface 'insert' => 'setInsert', 'update' => 'setUpdate', 'upsert' => 'setInsertOrUpdate', - 'replace' => 'replace', + 'replace' => 'setReplace', + 'delete' => 'setDelete' ]; /** @@ -450,29 +451,44 @@ public function commit(array $args = []) foreach ($inputMutations as $mutation) { $type = array_keys($mutation)[0]; $data = $mutation[$type]; - $data['values'] = $this->formatListForApi($data['values']); switch ($type) { case 'insert': case 'update': case 'upsert': case 'replace': - $write = (new Mutation\Write) - ->deserialize($data, $this->codec); + $data['values'] = $this->formatListForApi($data['values']); - $setterName = $this->mutationSetters[$type]; - $mutation = new Mutation; - $mutation->$setterName($write); - $mutations[] = $mutation; + $operation = (new Mutation\Write) + ->deserialize($data, $this->codec); break; case 'delete': - $mutations[] = (new Mutation\Delete) + if (isset($data['keySet']['keys'])) { + $data['keySet']['keys'] = $this->formatListForApi($data['keySet']['keys']); + } + + if (isset($data['keySet']['ranges'])) { + foreach ($data['keySet']['ranges'] as $index => $rangeItem) { + foreach ($rangeItem as $key => $val) { + $rangeItem[$key] = $this->formatListForApi($val); + } + + $data['keySet']['ranges'][$index] = $rangeItem; + } + } + + $operation = (new Mutation\Delete) ->deserialize($data, $this->codec); break; } + + $setterName = $this->mutationSetters[$type]; + $mutation = new Mutation; + $mutation->$setterName($operation); + $mutations[] = $mutation; } } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 5476ccecc186..e6406d09a8fa 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Spanner; +use Google\Cloud\ArrayTrait; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; @@ -50,6 +51,8 @@ */ class Database { + use ArrayTrait; + /** * @var ConnectionInterface */ @@ -294,13 +297,34 @@ public function iam() } /** - * Create a Read Only transaction + * Create a Read Only transaction. + * + * If no configuration options are provided, transaction will be opened with + * strong consistency. * * @codingStandardsIgnoreStart * @param array $options [optional] { * Configuration Options * - * @type array $transactionOptions [TransactionOptions](https://cloud.google.com/spanner/reference/rest/v1/TransactionOptions). + * 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`, `$minReadTimestamp`, `$maxStaleness`, + * `$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 $minReadTimestamp Executes all reads at a timestamp + * greater than or equal to the given timestamp. + * @type int $maxStaleness Represents a number of seconds. Read data at + * a timestamp greater than or equal to the current time minus the + * given number of seconds. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type int $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. * } * @codingStandardsIgnoreEnd * @return Transaction @@ -308,14 +332,47 @@ public function iam() public function readOnlyTransaction(array $options = []) { $options += [ - 'transactionOptions' => [] + 'returnReadTimestamp' => null, + 'strong' => null, + 'minReadTimestamp' => null, + 'maxStaleness' => null, + 'readTimestamp' => null, + 'exactStaleness' => null + ]; + + $options['transactionOptions'] = [ + 'readOnly' => $this->arrayFilterPreserveBool([ + 'returnReadTimestamp' => $this->pluck('returnReadTimestamp', $options), + 'strong' => $this->pluck('strong', $options), + 'minReadTimestamp' => $this->pluck('minReadTimestamp', $options), + 'maxStaleness' => $this->pluck('maxStaleness', $options), + 'readTimestamp' => $this->pluck('readTimestamp', $options), + 'exactStaleness' => $this->pluck('exactStaleness', $options), + ]) ]; - if (empty($options['transactionOptions'])) { - $options['transactionOptions']['strong'] = true; + if (empty($options['transactionOptions']['readOnly'])) { + $options['transactionOptions']['readOnly']['strong'] = true; } - $options['readOnly'] = $options['transactionOptions']; + $timestampFields = [ + 'minReadTimestamp', + 'readTimestamp' + ]; + + foreach ($timestampFields as $tsf) { + if (isset($options['transactionOptions']['readOnly'][$tsf])) { + $field = $options['transactionOptions']['readOnly'][$tsf]; + if (!($field instanceof Timestamp)) { + throw new \InvalidArgumentException(sprintf( + 'Read Only Transaction Configuration Field %s must be an instance of Timestamp', + $tsf + )); + } + + $options['transactionOptions']['readOnly'][$tsf] = $field->formatAsString(); + } + } return $this->transaction(SessionPoolInterface::CONTEXT_READ, $options); } @@ -328,7 +385,9 @@ public function readOnlyTransaction(array $options = []) */ public function lockingTransaction(array $options = []) { - $options['readWrite'] = []; + $options['transactionOptions'] = [ + 'readWrite' => [] + ]; return $this->transaction(SessionPoolInterface::CONTEXT_READWRITE, $options); } @@ -466,32 +525,16 @@ public function replaceBatch($table, array $dataSet, array $options = []) } /** - * Delete a row. - * - * @param string $table The table to mutate. - * @param array $key The key to use to identify the row or rows to delete. - * @param array $options [optional] Configuration options. - * @return array - */ - public function delete($table, array $key, array $options = []) - { - return $this->deleteBatch($table, [$key], $options); - } - - /** - * Delete multiple rows. + * Delete one or more rows. * * @param string $table The table to mutate. - * @param array $keySets The keys to use to identify the row or rows to delete. + * @param KeySet $keySet The KeySet to identify rows to delete. * @param array $options [optional] Configuration options. * @return array */ - public function deleteBatch($table, array $keySets, array $options = []) + public function delete($table, KeySet $keySet, array $options = []) { - $mutations = []; - foreach ($keySets as $keySet) { - $mutations[] = $this->operation->deleteMutation($table, $keySet); - } + $mutations = [$this->operation->deleteMutation($table, $keySet)]; $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php index 729e8c43408f..37b9f090c7b0 100644 --- a/src/Spanner/KeyRange.php +++ b/src/Spanner/KeyRange.php @@ -17,75 +17,185 @@ namespace Google\Cloud\Spanner; +/** + * Represents a Google Cloud Spanner KeyRange. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.KeyRange KeyRange + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * // Create a KeyRange for all people named Bob, born in 1969. + * $start = $spanner->date(new \DateTime('1969-01-01')); + * $end = $spanner->date(new \DateTime('1969-12-31')); + * + * $range = $spanner->keyRange([ + * 'startType' => KeyRange::TYPE_CLOSED, + * 'start' => ['Bob', $start], + * 'endType' => KeyRange::TYPE_CLOSED, + * 'end' => ['Bob', $end] + * ]); + * ``` + */ class KeyRange { + const TYPE_OPEN = 'open'; + const TYPE_CLOSED = 'closed'; + /** - * @var mixed + * @var array */ - private $startOpen; + private $types = []; /** - * @var mixed + * @var array */ - private $startClosed; + private $range = []; /** - * @var mixed + * @var array */ - private $endOpen; + private $definition = [ + self::TYPE_OPEN => [ + 'start' => 'startOpen', + 'end' => 'endOpen' + ], + self::TYPE_CLOSED => [ + 'start' => 'startClosed', + 'end' => 'endClosed' + ] + ]; /** - * @var mixed + * Create a KeyRange. + * + * @param array $options [optional] { + * Configuration Options. + * + * @type string $startType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $start The key with which to start the range. + * @type string $endType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $end The key with which to end the range. + * } */ - private $endClosed; - - public function __construct(array $range) + public function __construct(array $options = []) { - $this->startOpen = (isset($range['startOpen'])) - ? $range['startOpen'] - : null; - - $this->startClosed = (isset($range['startClosed'])) - ? $range['startClosed'] - : null; + $options = array_filter($options + [ + 'startType' => null, + 'start' => [], + 'endType' => null, + 'end' => [] + ]); - $this->endOpen = (isset($range['endOpen'])) - ? $range['endOpen'] - : null; + if (isset($options['startType']) && isset($options['start'])) { + $this->setStart($options['startType'], $options['start']); + } - $this->endClosed = (isset($range['endClosed'])) - ? $range['endClosed'] - : null; + if (isset($options['endType']) && isset($options['end'])) { + $this->setEnd($options['endType'], $options['end']); + } + } + /** + * Get the range start. + * + * @return array + */ + public function start() + { + $type = $this->types['start']; + return $this->range[$this->definition[$type]['start']]; } - public function setStartOpen($startOpen) + /** + * Set the range start. + * + * @param string $type Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed + * correctness. + * @param array $start The start of the key range. + * @return void + */ + public function setStart($type, array $start) { - $this->startOpen = $startOpen; + if (!in_array($type, array_keys($this->definition))) { + throw new \InvalidArgumentException(sprintf( + 'Invalid KeyRange type. Allowed values are %s', + implode(', ', array_keys($this->definition)) + )); + } + + $rangeKey = $this->definition[$type]['start']; + + $this->types['start'] = $type; + $this->range[$rangeKey] = $start; } - public function setStartClosed($startClosed) + /** + * Get the range end. + * + * @return array + */ + public function end() { - $this->startClosed = $startClosed; + $type = $this->types['end']; + return $this->range[$this->definition[$type]['end']]; } - public function setEndOpen($endOpen) + /** + * Set the range end. + * + * @param string $type Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed + * correctness. + * @param array $end The end of the key range. + * @return void + */ + public function setEnd($type, array $end) { - $this->endOpen = $endOpen; + if (!in_array($type, array_keys($this->definition))) { + throw new \InvalidArgumentException(sprintf( + 'Invalid KeyRange type. Allowed values are %s', + implode(', ', array_keys($this->definition)) + )); + } + + $rangeKey = $this->definition[$type]['end']; + + $this->types['end'] = $type; + $this->range[$rangeKey] = $end; } - public function setEndClosed($endClosed) + /** + * Get the start and end types + * + * @return array + */ + public function types() { - $this->endClosed = $endClosed; + return $this->types; } + /** + * Returns an API-compliant representation of a KeyRange. + * + * @return array + * @access private + */ public function keyRangeObject() { - return [ - 'startOpen' => $this->startOpen, - 'startClosed' => $this->startClosed, - 'endOpen' => $this->endOpen, - 'endClosed' => $this->endClosed, - ]; + if (count($this->range) !== 2) { + throw new \BadMethodCallException('Key Range must supply a start and an end'); + } + + return $this->range; } } diff --git a/src/Spanner/KeySet.php b/src/Spanner/KeySet.php index 1de1af5c4f1e..b8390f72de09 100644 --- a/src/Spanner/KeySet.php +++ b/src/Spanner/KeySet.php @@ -43,6 +43,18 @@ class KeySet */ private $all; + /** + * Create a KeySet. + * + * @param array $options [optional] { + * @type array $keys A list of specific keys. Entries in keys should + * have exactly as many elements as there are columns in the + * primary or index key with which this KeySet is used. + * @type KeyRange[] $ranges A list of Key Ranges. + * @type bool $all If true, KeySet will match all keys in a table. + * **Defaults to** `false`. + * } + */ public function __construct(array $options = []) { $options += [ @@ -58,11 +70,36 @@ public function __construct(array $options = []) $this->all = (bool) $options['all']; } + /** + * Fetch the KeyRanges + * + * @return KeyRange[] + */ + public function ranges() + { + return $this->ranges; + } + + + /** + * Add a single KeyRange. + * + * @param KeyRange $range A KeyRange instance. + * @return void + */ public function addRange(KeyRange $range) { $this->ranges[] = $range; } + /** + * Set the KeySet's KeyRanges. + * + * Any existing KeyRanges will be overridden. + * + * @param KeyRange[] $ranges An array of KeyRange objects. + * @return void + */ public function setRanges(array $ranges) { $this->validateBatch($ranges, KeyRange::class); @@ -70,21 +107,69 @@ public function setRanges(array $ranges) $this->ranges = $ranges; } + /** + * Fetch the keys. + * + * @return mixed[] + */ + public function keys() + { + return $this->keys; + } + + /** + * Add a single key. + * + * A Key should have exactly as many elements as there are columns in the + * primary or index key with which this KeySet is used. + * + * @param mixed $key The Key to add. + * @return void + */ public function addKey($key) { $this->keys[] = $key; } + /** + * Set the KeySet keys. + * + * Any existing keys will be overridden. + * + * @param mixed[] $keys + * @return void + */ public function setKeys(array $keys) { $this->keys = $keys; } - public function setAll($all) + /** + * Get the value of Match All. + * + * @return bool + */ + public function matchAll() + { + return $this->all; + } + + /** + * Choose whether the KeySet should match all keys in a table. + * + * @param bool $all If true, all keys in a table will be matched. + * @return void + */ + public function setMatchAll($all) { $this->all = (bool) $all; } + /** + * Format a KeySet object for use in the Spanner API. + * + * @access private + */ public function keySetObject() { $ranges = []; diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index f743a514d0b8..5fa5396d1e88 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -38,6 +38,7 @@ class Operation const OP_UPDATE = 'update'; const OP_INSERT_OR_UPDATE = 'insertOrUpdate'; const OP_REPLACE = 'replace'; + const OP_DELETE = 'delete'; /** * @var ConnectionInterface @@ -90,15 +91,38 @@ public function mutation($operation, $table, $mutation) * Create a formatted delete mutation. * * @param string $table The table name. - * @param array $keySet [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). + * @param KeySet $keySet The keys to delete. * @return array */ - public function deleteMutation($table, $keySet) + public function deleteMutation($table, KeySet $keySet) { + $keyRanges = $keySet->ranges(); + if ($keyRanges) { + $ranges = []; + foreach ($keyRanges as $range) { + $types = $range->types(); + + $start = $range->start(); + $range->setStart($types['start'], $this->mapper->encodeValuesAsSimpleType($start)); + + $end = $range->end(); + $range->setEnd($types['end'], $this->mapper->encodeValuesAsSimpleType($end)); + + $ranges[] = $range; + } + + $keySet->setRanges($ranges); + } + + $keys = $keySet->keySetObject(); + if (!empty($keys['keys'])) { + $keys['keys'] = $this->mapper->encodeValuesAsSimpleType($keys['keys']); + } + return [ - 'delete' => [ + self::OP_DELETE => [ 'table' => $table, - 'keySet' => $keySet + 'keySet' => $this->arrayFilterPreserveBool($keys) ] ]; } @@ -119,19 +143,12 @@ public function commit(Session $session, array $mutations, array $options = []) $options['singleUseTransaction'] = ['readWrite' => []]; } - try { - $res = $this->connection->commit([ - 'mutations' => $mutations, - 'session' => $session->name() - ] + $options); - - return $res; - } catch (\Exception $e) { - - // maybe do something here? + $res = $this->connection->commit([ + 'mutations' => $mutations, + 'session' => $session->name() + ] + $options); - throw $e; - } + return $res; } /** @@ -208,7 +225,7 @@ public function read(Session $session, $table, array $options = []) if (empty($options['keySet'])) { $options['keySet'] = new KeySet(); - $options['keySet']->setAll(true); + $options['keySet']->setMatchAll(true); } $options['keySet'] = $options['keySet']->keySetObject(); diff --git a/src/Spanner/Session/SessionPoolInterface.php b/src/Spanner/Session/SessionPoolInterface.php index 28662a090ae9..1294f2571d6a 100644 --- a/src/Spanner/Session/SessionPoolInterface.php +++ b/src/Spanner/Session/SessionPoolInterface.php @@ -19,8 +19,8 @@ interface SessionPoolInterface { - const CONTEXT_READ = 'read'; - const CONTEXT_READWRITE = 'readWrite'; + const CONTEXT_READ = 'r'; + const CONTEXT_READWRITE = 'rw'; public function session($instance, $database, $context, array $options = []); } diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index aa2e8c4a2e3b..e6647592a0e7 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -326,12 +326,23 @@ public function keySet(array $options = []) /** * Create a new KeyRange object * - * @param array $range [optional] The key range data. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $startType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $start The key with which to start the range. + * @type string $endType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $end The key with which to end the range. + * } * @return KeyRange */ - public function keyRange(array $range = []) + public function keyRange(array $options = []) { - return new KeyRange($range); + return new KeyRange($options); } /** diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index 45c6bbb66631..e39d23144ff6 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -880,7 +880,7 @@ public function commit($session, $mutations, $optionalArgs = []) $mergedSettings, $this->descriptors['commit'] ); - +// print_r($request->serialize(new \Google\Cloud\PhpArray));exit; return $callable( $request, [], diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php new file mode 100644 index 000000000000..66cb7fb5975d --- /dev/null +++ b/tests/unit/Spanner/DatabaseTest.php @@ -0,0 +1,406 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->instance = $this->prophesize(Instance::class); + $this->sessionPool = $this->prophesize(SessionPoolInterface::class); + $this->sessionPool->session(self::INSTANCE, self::DATABASE, Argument::any()) + ->willReturn(new Session( + $this->connection->reveal(), + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + )); + + $this->instance->name()->willReturn(self::INSTANCE); + + $args = [ + $this->connection->reveal(), + $this->instance->reveal(), + $this->sessionPool->reveal(), + self::PROJECT, + self::DATABASE, + ]; + + $props = [ + 'connection', 'operation' + ]; + + $this->database = \Google\Cloud\Dev\stub(Database::class, $args, $props); + } + + public function testReadOnlyTransaction() + { + $this->connection->beginTransaction(Argument::that(function($arg) { + if ($arg['transactionOptions']['readOnly']['strong'] !== TRUE) return false; + + return true; + })) + ->shouldBeCalled() + ->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->database->setConnection($this->connection->reveal()); + + $t = $this->database->readOnlyTransaction(); + $this->assertInstanceOf(Transaction::class, $t); + } + + public function testReadOnlyTransactionOptions() + { + $options = [ + 'returnReadTimestamp' => true, + 'strong' => false, + 'minReadTimestamp' => new Timestamp(new \DateTime), + 'maxStaleness' => 1337, + 'readTimestamp' => new Timestamp(new \DateTime), + 'exactStaleness' => 7331 + ]; + + $this->connection->beginTransaction(Argument::that(function($arg) use ($options) { + if ($arg['transactionOptions']['readOnly']['returnReadTimestamp'] !== $options['returnReadTimestamp']) return false; + if ($arg['transactionOptions']['readOnly']['strong'] !== $options['strong']) return false; + if ($arg['transactionOptions']['readOnly']['minReadTimestamp'] !== $options['minReadTimestamp']->formatAsString()) return false; + if ($arg['transactionOptions']['readOnly']['maxStaleness'] !== $options['maxStaleness']) return false; + if ($arg['transactionOptions']['readOnly']['readTimestamp'] !== $options['readTimestamp']->formatAsString()) return false; + if ($arg['transactionOptions']['readOnly']['exactStaleness'] !== $options['exactStaleness']) return false; + + return true; + })) + ->shouldBeCalled() + ->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->database->setConnection($this->connection->reveal()); + + $this->database->readOnlyTransaction($options); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testReadOnlyTransactionInvalidConfigType() + { + $t = $this->database->readOnlyTransaction(['minReadTimestamp' => 'foo']); + } + + public function testLockingTransaction() + { + $this->connection->beginTransaction(Argument::that(function($arg) { + if (!isset($arg['transactionOptions']['readWrite'])) return false; + + return true; + })) + ->shouldBeCalled() + ->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->database->setConnection($this->connection->reveal()); + + $t = $this->database->lockingTransaction(); + $this->assertInstanceOf(Transaction::class, $t); + } + + public function testInsert() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->insert($table, $row); + $this->assertEquals('res', $res); + } + + public function testInsertBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->insertBatch($table, [$row]); + $this->assertEquals('res', $res); + } + + public function testUpdate() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->update($table, $row); + $this->assertEquals('res', $res); + } + + public function testUpdateBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->updateBatch($table, [$row]); + $this->assertEquals('res', $res); + } + + public function testInsertOrUpdate() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->insertOrUpdate($table, $row); + $this->assertEquals('res', $res); + } + + public function testInsertOrUpdateBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->insertOrUpdateBatch($table, [$row]); + $this->assertEquals('res', $res); + } + + public function testReplace() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_REPLACE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->replace($table, $row); + $this->assertEquals('res', $res); + } + + public function testReplaceBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_REPLACE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->replaceBatch($table, [$row]); + $this->assertEquals('res', $res); + } + + public function testDelete() + { + $table = 'foo'; + $keys = [10, 'bar']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $keys) { + if ($arg['mutations'][0][Operation::OP_DELETE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][0] !== (string) $keys[0]) return false; + if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][1] !== $keys[1]) return false; + + return true; + }))->shouldBeCalled()->willReturn('res'); + + $this->refreshOperation(); + + $res = $this->database->delete($table, new KeySet(['keys' => $keys])); + $this->assertEquals('res', $res); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Table'; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { + if ($arg['sql'] !== $sql) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->database->execute($sql); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $table = 'Table'; + $opts = ['foo' => 'bar']; + + $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { + if ($arg['table'] !== $table) return false; + if ($arg['foo'] !== $opts['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->database->read($table, $opts); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + // ******* + // Helpers + + private function refreshOperation() + { + $operation = new Operation($this->connection->reveal(), false); + $this->database->setOperation($operation); + } +} diff --git a/tests/unit/Spanner/KeyRangeTest.php b/tests/unit/Spanner/KeyRangeTest.php index 9b50d5259f9b..c276ba677d5e 100644 --- a/tests/unit/Spanner/KeyRangeTest.php +++ b/tests/unit/Spanner/KeyRangeTest.php @@ -24,41 +24,72 @@ */ class KeyRangeTest extends \PHPUnit_Framework_TestCase { - private $startOpen = 'startOpen'; - private $startClosed = 'startClosed'; - private $endOpen = 'endOpen'; - private $endClosed = 'endClosed'; + private $range; - public function testSetters() + public function setUp() { - $kr = new KeyRange([]); - $kr->setStartOpen($this->startOpen); - $kr->setStartClosed($this->startClosed); - $kr->setEndOpen($this->endOpen); - $kr->setEndClosed($this->endClosed); - - $this->assertThings($kr); + $this->range = new KeyRange; } - public function testConstructValues() + public function testGetters() { - $kr = new KeyRange([ - 'startOpen' => $this->startOpen, - 'startClosed' => $this->startClosed, - 'endOpen' => $this->endOpen, - 'endClosed' => $this->endClosed + $range = new KeyRange([ + 'startType' => KeyRange::TYPE_CLOSED, + 'start' => ['foo'], + 'endType' => KeyRange::TYPE_OPEN, + 'end' => ['bar'] ]); - $this->assertThings($kr); + $this->assertEquals(['foo'], $range->start()); + $this->assertEquals(['bar'], $range->end()); + $this->assertEquals(['start' => KeyRange::TYPE_CLOSED, 'end' => KeyRange::TYPE_OPEN], $range->types()); + } + + public function testSetStart() + { + $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']); + $this->assertEquals(['foo'], $this->range->start()); + $this->assertEquals(KeyRange::TYPE_OPEN, $this->range->types()['start']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testSetStartInvalidType() + { + $this->range->setStart('foo', ['foo']); + } + + public function testSetEnd() + { + $this->range->setEnd(KeyRange::TYPE_OPEN, ['foo']); + $this->assertEquals(['foo'], $this->range->end()); + $this->assertEquals(KeyRange::TYPE_OPEN, $this->range->types()['end']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testSetEndInvalidType() + { + $this->range->setEnd('foo', ['foo']); + } + + public function testKeyRangeObject() + { + $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']); + $this->range->setEnd(KeyRange::TYPE_CLOSED, ['bar']); + + $res = $this->range->keyRangeObject(); + + $this->assertEquals(['startOpen' => ['foo'], 'endClosed' => ['bar']], $res); } - private function assertThings($kr) + /** + * @expectedException BadMethodCallException + */ + public function testKeyRangeObjectBadRange() { - $this->assertEquals([ - 'startOpen' => $this->startOpen, - 'startClosed' => $this->startClosed, - 'endOpen' => $this->endOpen, - 'endClosed' => $this->endClosed - ], $kr->keyRangeObject()); + $this->range->keyRangeObject(); } } diff --git a/tests/unit/Spanner/KeySetTest.php b/tests/unit/Spanner/KeySetTest.php index be4efcb52cb1..75551a6dc946 100644 --- a/tests/unit/Spanner/KeySetTest.php +++ b/tests/unit/Spanner/KeySetTest.php @@ -28,30 +28,33 @@ class KeySetTest extends \PHPUnit_Framework_TestCase public function testAddRange() { $set = new KeySet; - $range = new KeyRange(['setStartOpen' => 'foo']); + $range = $this->prophesize(KeyRange::class); + $range->keyRangeObject()->willReturn('foo'); - $set->addRange($range); + $set->addRange($range->reveal()); - $this->assertEquals($range->keyRangeObject(), $set->keySetObject()['ranges'][0]); + $this->assertEquals('foo', $set->keySetObject()['ranges'][0]); } public function testSetRanges() { $set = new KeySet; + $range1 = $this->prophesize(KeyRange::class); + $range1->keyRangeObject()->willReturn('foo'); + + $range2 = $this->prophesize(KeyRange::class); + $range2->keyRangeObject()->willReturn('bar'); + $ranges = [ - new KeyRange(['setStartOpen' => 'foo']), - new KeyRange(['setStartOpen' => 'bar']), + $range1->reveal(), + $range2->reveal() ]; $set->setRanges($ranges); - $expected = []; - foreach ($ranges as $r) { - $expected[] = $r->keyRangeObject(); - } - - $this->assertEquals($expected, $set->keySetObject()['ranges']); + $this->assertEquals('foo', $set->keySetObject()['ranges'][0]); + $this->assertEquals('bar', $set->keySetObject()['ranges'][1]); } public function testAddKey() @@ -76,14 +79,41 @@ public function testSetKeys() $this->assertEquals($keys, $set->keySetObject()['keys']); } - public function testSetAll() + public function testSetMatchAll() { $set = new KeySet; - $set->setAll(true); + $set->setMatchAll(true); $this->assertTrue($set->keySetObject()['all']); - $set->setAll(false); + $set->setMatchAll(false); $this->assertFalse($set->keySetObject()['all']); } + + public function testRanges() + { + $set = new KeySet; + $range = $this->prophesize(KeyRange::class)->reveal(); + + $set->addRange($range); + $this->assertEquals($range, $set->ranges()[0]); + } + + public function testKeys() + { + $set = new KeySet; + $key = 'foo'; + $set->addKey($key); + + $this->assertEquals($key, $set->keys()[0]); + } + + public function testMatchAll() + { + $set = new KeySet(); + $this->assertFalse($set->matchAll()); + + $set->setMatchAll(true); + $this->assertTrue($set->matchAll()); + } } From 77c390e0a72e74a7eea2a46c761b3dd056aceb97 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 9 Jan 2017 13:16:51 -0500 Subject: [PATCH 028/107] Additional tests and miscellaneous improvements --- src/Spanner/Connection/Grpc.php | 2 +- src/Spanner/Database.php | 212 +++++++++++++++++++++-- src/Spanner/Instance.php | 1 - src/Spanner/Operation.php | 5 +- src/Spanner/Transaction.php | 133 +++++++++----- src/Spanner/ValueMapper.php | 23 ++- tests/unit/Spanner/DatabaseTest.php | 57 ++++-- tests/unit/SpannerAdmin/InstanceTest.php | 25 +++ 8 files changed, 373 insertions(+), 85 deletions(-) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 54ddc9462508..2e5d1c34e507 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -75,7 +75,7 @@ public function __construct(array $config = []) { $this->codec = new PhpArray([ 'customFilters' => [ - 'timestamp' => function ($v) { + 'commitTimestamp' => function ($v) { return $this->formatTimestampFromApi($v); } ] diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index e6406d09a8fa..3a1da137f6e7 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -20,10 +20,10 @@ use Google\Cloud\ArrayTrait; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\V1\SpannerClient as GrpcSpannerClient; /** * Represents a Google Cloud Spanner Database @@ -303,6 +303,10 @@ public function iam() * strong consistency. * * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @codingStandardsIgnoreEnd + * + * @codingStandardsIgnoreStart * @param array $options [optional] { * Configuration Options * @@ -380,6 +384,10 @@ public function readOnlyTransaction(array $options = []) /** * Create a Read/Write transaction * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration Options * @return Transaction */ @@ -395,10 +403,23 @@ public function lockingTransaction(array $options = []) /** * Insert a row. * + * Example: + * ``` + * $database->insert('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Welcome to our site.' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $data The row data to insert. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function insert($table, array $data, array $options = []) { @@ -408,10 +429,29 @@ public function insert($table, array $data, array $options = []) /** * Insert multiple rows. * + * Example: + * ``` + * $database->insert('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Welcome to our site.' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'Lots of people ask about where we got started.' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $dataSet The row data to insert. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function insertBatch($table, array $dataSet, array $options = []) { @@ -428,10 +468,26 @@ public function insertBatch($table, array $dataSet, array $options = []) /** * Update a row. * + * Only data which you wish to update need be included. You must provide + * enough information for the API to determine which row should be modified. + * In most cases, this means providing values for the Primary Key fields. + * + * Example: + * ``` + * $database->update('Posts', [ + * 'ID' => 1337, + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $data The row data to update. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function update($table, array $data, array $options = []) { @@ -441,10 +497,31 @@ public function update($table, array $data, array $options = []) /** * Update multiple rows. * + * Only data which you wish to update need be included. You must provide + * enough information for the API to determine which row should be modified. + * In most cases, this means providing values for the Primary Key fields. + * + * Example: + * ``` + * $database->update('Posts', [ + * [ + * 'ID' => 1337, + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $dataSet The row data to update. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function updateBatch($table, array $dataSet, array $options = []) { @@ -461,10 +538,27 @@ public function updateBatch($table, array $dataSet, array $options = []) /** * Insert or update a row. * + * If a row already exists (determined by comparing the Primary Key to + * existing table data), the row will be updated. If not, it will be + * created. + * + * Example: + * ``` + * $database->insertOrUpdate('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $data The row data to insert or update. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function insertOrUpdate($table, array $data, array $options = []) { @@ -474,10 +568,33 @@ public function insertOrUpdate($table, array $data, array $options = []) /** * Insert or update multiple rows. * + * If a row already exists (determined by comparing the Primary Key to + * existing table data), the row will be updated. If not, it will be + * created. + * + * Example: + * ``` + * $database->insertOrUpdateBatch('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $dataSet The row data to insert or update. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) { @@ -494,10 +611,27 @@ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) /** * Replace a row. * + * Provide data for the entire row. Google Cloud Spanner will attempt to + * find a record matching the Primary Key, and will replace the entire row. + * If a matching row is not found, it will be inserted. + * + * Example: + * ``` + * $database->replace('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $data The row data to replace. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function replace($table, array $data, array $options = []) { @@ -507,10 +641,33 @@ public function replace($table, array $data, array $options = []) /** * Replace multiple rows. * + * Provide data for the entire row. Google Cloud Spanner will attempt to + * find a record matching the Primary Key, and will replace the entire row. + * If a matching row is not found, it will be inserted. + * + * Example: + * ``` + * $database->replaceBatch('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $dataSet The row data to replace. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function replaceBatch($table, array $dataSet, array $options = []) { @@ -527,10 +684,25 @@ public function replaceBatch($table, array $dataSet, array $options = []) /** * Delete one or more rows. * + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [ + * 1337, 1338 + * ] + * ]); + * + * $database->delete('Posts', $keySet); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param KeySet $keySet The KeySet to identify rows to delete. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function delete($table, KeySet $keySet, array $options = []) { @@ -547,14 +719,19 @@ public function delete($table, KeySet $keySet, array $options = []) * Example: * ``` * $result = $spanner->execute( - * 'SELECT * FROM Users WHERE id = @userId', + * 'SELECT * FROM Posts WHERE ID = @postId', * [ * 'parameters' => [ - * 'userId' => 1 + * 'postId' => 1337 * ] * ] * ); * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ExecuteSqlRequest ExecuteSqlRequest + * @codingStandardsIgnoreEnd + * * @param string $sql The query string to execute. * @param array $options [optional] { * Configuration options. @@ -578,7 +755,7 @@ public function execute($sql, array $options = []) * Note that if no KeySet is specified, all rows in a table will be * returned. * - * @todo is returning everything a reasonable default? + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest ReadRequest * * @param string $table The table name. * @param array $options [optional] { @@ -601,6 +778,8 @@ public function read($table, array $options = []) /** * Create a transaction with a given context. * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * * @param string $context The context of the new transaction. * @param array $options [optional] Configuration options. * @return Transaction @@ -643,7 +822,7 @@ private function selectSession($context = SessionPoolInterface::CONTEXT_READ) { */ private function fullyQualifiedDatabaseName() { - return DatabaseAdminClient::formatDatabaseName( + return GrpcSpannerClient::formatDatabaseName( $this->projectId, $this->instance->name(), $this->name @@ -661,7 +840,10 @@ public function __debugInfo() return [ 'connection' => get_class($this->connection), 'projectId' => $this->projectId, - 'name' => $this->name + 'name' => $this->name, + 'instance' => $this->instance, + 'sessionPool' => $this->sessionPool, + 'returnInt64AsObject' => $this->returnInt64AsObject, ]; } } diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 26658c22cba8..af8fc8c97c31 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -369,7 +369,6 @@ public function database($name) public function databases(array $options = []) { $pageToken = null; - do { $res = $this->connection->listDatabases($options + [ 'instance' => $this->fullyQualifiedInstanceName(), diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 5fa5396d1e88..1a66253c1e00 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -134,8 +134,7 @@ public function deleteMutation($table, KeySet $keySet) * @param Session $session The session ID to use for the commit. * @param array $mutations The mutations to commit. * @param array $options [optional] Configuration options. - * @return array [CommitResponse](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitResponse) - * @codingStandardsIgnoreEnd + * @return Timestamp The commit Timestamp. */ public function commit(Session $session, array $mutations, array $options = []) { @@ -148,7 +147,7 @@ public function commit(Session $session, array $mutations, array $options = []) 'session' => $session->name() ] + $options); - return $res; + return $this->mapper->createTimestampWithNanos($res['commitTimestamp']); } /** diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 791bcdca6664..3b70acbc89d9 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -82,17 +82,25 @@ public function __construct( * * @param string $table The table to insert into. * @param array $data The data to insert. - * @return void + * @return Transaction The transaction, to enable method chaining. */ public function insert($table, array $data) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + return $this->insertBatch($table, [$data]); + } + + /** + * Enqueue one or more insert mutations. + * + * @param string $table The table to insert into. + * @param array $dataSet The data to insert. + * @return Transaction The transaction, to enable method chaining. + */ + public function insertBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_INSERT, $table, $dataSet); - $this->mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data); + return $this; } /** @@ -100,17 +108,25 @@ public function insert($table, array $data) * * @param string $table The table to update. * @param array $data The data to update. - * @return void + * @return Transaction The transaction, to enable method chaining. */ public function update($table, array $data) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + return $this->updateBatch($table, [$data]); + } - $this->mutations[] = $this->operation->mutation(Operation::OP_UPDATE, $table, $data); + /** + * Enqueue one or more update mutations. + * + * @param string $table The table to update. + * @param array $dataSet The data to update. + * @return Transaction The transaction, to enable method chaining. + */ + public function updateBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_UPDATE, $table, $dataSet); + + return $this; } /** @@ -118,17 +134,25 @@ public function update($table, array $data) * * @param string $table The table to insert into or update. * @param array $data The data to insert or update. - * @return void + * @return Transaction The transaction, to enable method chaining. */ public function insertOrUpdate($table, array $data) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + return $this->insertOrUpdateBatch($table, [$data]); + } + + /** + * Enqueue one or more insert or update mutations. + * + * @param string $table The table to insert into or update. + * @param array $dataSet The data to insert or update. + * @return Transaction The transaction, to enable method chaining. + */ + public function insertOrUpdateBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_INSERT_OR_UPDATE, $table, $dataSet); - $this->mutations[] = $this->operation->mutation(Operation::OP_INSERT_OR_UPDATE, $table, $data); + return $this; } /** @@ -136,35 +160,39 @@ public function insertOrUpdate($table, array $data) * * @param string $table The table to replace into. * @param array $data The data to replace. - * @return void + * @return Transaction The transaction, to enable method chaining. */ public function replace($table, array $data) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + return $this->replaceBatch($table, [$data]); + } - $this->mutations[] = $this->operation->mutation(Operation::OP_REPLACE, $table, $data); + /** + * Enqueue one or more replace mutations. + * + * @param string $table The table to replace into. + * @param array $dataSet The data to replace. + * @return Transaction The transaction, to enable method chaining. + */ + public function replaceBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_REPLACE, $table, $dataSet); + + return $this; } /** * Enqueue an delete mutation. * - * @param string $table The table to delete from. - * @param array $key The key of the record to be deleted. - * @return void + * @param string $table The table to mutate. + * @param KeySet $keySet The KeySet to identify rows to delete. + * @return Transaction The transaction, to enable method chaining. */ - public function delete($table, array $key) + public function delete($table, KeySet $keySet) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + $this->enqueue(Operation::OP_DELETE, $table, [$keySet]); - $this->mutations[] = $this->operation->deleteMutation($table, $data); + return $this; } /** @@ -229,10 +257,8 @@ public function read($table, array $options = []) * * This closes the transaction, preventing any future API calls inside it. * - * @codingStandardsIgnoreStart * @param array $options [optional] Configuration Options. - * @return array [Response Body](https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases.sessions/commit#response-body). - * @codingStandardsIgnoreEnd + * @return Timestamp The commit Timestamp. */ public function commit(array $options = []) { @@ -263,4 +289,29 @@ public function rollback(array $options = []) { return $this->operation->rollback($this->session, $this->transactionId, $options); } + + /** + * Format, validate and enqueue mutations in the transaction. + * + * @param string $op The operation type. + * @param string $table The table name + * @param array $dataSet the mutations to enqueue + * @return void + */ + private function enqueue($op, $table, array $dataSet) + { + if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new RuntimeException( + 'Cannot perform mutations in a Read-Only Transaction' + ); + } + + foreach ($dataSet as $data) { + if ($op === Operation::OP_DELETE) { + $this->mutations[] = $this->operation->deleteMutation($table, $data); + } else { + $this->mutations[] = $this->operation->mutation($op, $table, $data); + } + } + } } diff --git a/src/Spanner/ValueMapper.php b/src/Spanner/ValueMapper.php index 0887deb358be..ff45226c9e24 100644 --- a/src/Spanner/ValueMapper.php +++ b/src/Spanner/ValueMapper.php @@ -116,6 +116,22 @@ public function decodeValues(array $columns, array $row, $extractResult = false) return $res; } + /** + * Convert a timestamp string to a Timestamp class with nanosecond support. + * + * @param string $timestamp The timestamp string + * @return Timestamp + */ + public function createTimestampWithNanos($timestamp) + { + $matches = []; + preg_match(self::NANO_REGEX, $timestamp, $matches); + $timestamp = preg_replace(self::NANO_REGEX, '.000000Z', $timestamp); + + $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $timestamp); + return new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0); + } + /** * Convert a single value to its corresponding PHP type. * @@ -133,12 +149,7 @@ private function decodeValue($value, array $type) break; case self::TYPE_TIMESTAMP: - $matches = []; - preg_match(self::NANO_REGEX, $value, $matches); - $value = preg_replace(self::NANO_REGEX, '.000000Z', $value); - - $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $value); - $value = new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0); + $value = $this->createTimestampWithNanos($value); break; case self::TYPE_DATE: diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php index 66cb7fb5975d..4e038906dee5 100644 --- a/tests/unit/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/DatabaseTest.php @@ -163,12 +163,13 @@ public function testInsert() if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->insert($table, $row); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testInsertBatch() @@ -182,12 +183,13 @@ public function testInsertBatch() if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->insertBatch($table, [$row]); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testUpdate() @@ -201,12 +203,13 @@ public function testUpdate() if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->update($table, $row); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testUpdateBatch() @@ -220,12 +223,13 @@ public function testUpdateBatch() if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->updateBatch($table, [$row]); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testInsertOrUpdate() @@ -239,12 +243,13 @@ public function testInsertOrUpdate() if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->insertOrUpdate($table, $row); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testInsertOrUpdateBatch() @@ -258,12 +263,13 @@ public function testInsertOrUpdateBatch() if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->insertOrUpdateBatch($table, [$row]); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testReplace() @@ -277,12 +283,13 @@ public function testReplace() if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->replace($table, $row); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testReplaceBatch() @@ -296,12 +303,13 @@ public function testReplaceBatch() if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->replaceBatch($table, [$row]); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testDelete() @@ -315,12 +323,13 @@ public function testDelete() if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][1] !== $keys[1]) return false; return true; - }))->shouldBeCalled()->willReturn('res'); + }))->shouldBeCalled()->willReturn($this->commitResponse()); $this->refreshOperation(); $res = $this->database->delete($table, new KeySet(['keys' => $keys])); - $this->assertEquals('res', $res); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); } public function testExecute() @@ -403,4 +412,16 @@ private function refreshOperation() $operation = new Operation($this->connection->reveal(), false); $this->database->setOperation($operation); } + + private function commitResponse() + { + return ['commitTimestamp' => '2017-01-09T18:05:22.534799Z']; + } + + private function assertTimestampIsCorrect($res) + { + $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']); + + $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z')); + } } diff --git a/tests/unit/SpannerAdmin/InstanceTest.php b/tests/unit/SpannerAdmin/InstanceTest.php index 960101d87e4d..9d38318f0a76 100644 --- a/tests/unit/SpannerAdmin/InstanceTest.php +++ b/tests/unit/SpannerAdmin/InstanceTest.php @@ -275,6 +275,31 @@ public function testDatabases() $this->assertEquals('database2', $dbs[1]->name()); } + public function testDatabasesPaged() + { + $databases = [ + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')], + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] + ]; + + $iteration = 0; + $this->connection->listDatabases(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn(['databases' => [$databases[0]], 'nextPageToken' => 'foo'], ['databases' => [$databases[1]]]); + + $this->instance->setConnection($this->connection->reveal()); + + $dbs = $this->instance->databases(); + + $this->assertInstanceOf(\Generator::class, $dbs); + + $dbs = iterator_to_array($dbs); + + $this->assertEquals(2, count($dbs)); + $this->assertEquals('database1', $dbs[0]->name()); + $this->assertEquals('database2', $dbs[1]->name()); + } + public function testIam() { $this->assertInstanceOf(Iam::class, $this->instance->iam()); From 9b18958ef5cf37083a90545c8313a6c7f6c2bad8 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 9 Jan 2017 17:17:48 -0500 Subject: [PATCH 029/107] Cover Transactions --- dev/src/Functions.php | 2 +- ...tStubPropertiesTrait.php => StubTrait.php} | 23 +- src/Spanner/Connection/Grpc.php | 7 +- src/Spanner/Database.php | 8 +- src/Spanner/Operation.php | 30 ++ src/Spanner/Transaction.php | 29 +- tests/unit/Spanner/TransactionTest.php | 332 ++++++++++++++++++ 7 files changed, 414 insertions(+), 17 deletions(-) rename dev/src/{SetStubPropertiesTrait.php => StubTrait.php} (76%) create mode 100644 tests/unit/Spanner/TransactionTest.php diff --git a/dev/src/Functions.php b/dev/src/Functions.php index 47612d9703b1..f38f42c391bf 100644 --- a/dev/src/Functions.php +++ b/dev/src/Functions.php @@ -8,7 +8,7 @@ function stub($extends, array $args = [], array $props = []) $props = ['connection']; } - $tpl = 'class %s extends %s {private $___props = \'%s\'; use \Google\Cloud\Dev\SetStubPropertiesTrait; }'; + $tpl = 'class %s extends %s {private $___props = \'%s\'; use \Google\Cloud\Dev\StubTrait; }'; $name = 'Stub'. sha1($extends); diff --git a/dev/src/SetStubPropertiesTrait.php b/dev/src/StubTrait.php similarity index 76% rename from dev/src/SetStubPropertiesTrait.php rename to dev/src/StubTrait.php index 2868852b8f37..8f0982680778 100644 --- a/dev/src/SetStubPropertiesTrait.php +++ b/dev/src/StubTrait.php @@ -17,8 +17,16 @@ namespace Google\Cloud\Dev; -trait SetStubPropertiesTrait +trait StubTrait { + public function ___getProperty($prop) + { + $property = $this->___getPropertyReflector($prop); + + $property->setAccessible(true); + return $property->getValue($this); + } + public function __call($method, $args) { $matches = []; @@ -32,16 +40,23 @@ public function __call($method, $args) throw new \BadMethodCallException(sprintf('Property %s cannot be overloaded', $prop)); } + $property = $this->___getPropertyReflector($prop); + + $property->setAccessible(true); + $property->setValue($this, $args[0]); + } + + private function ___getPropertyReflector($property) + { $trait = new \ReflectionClass($this); $ref = $trait->getParentClass(); try { - $property = $ref->getProperty($prop); + $property = $ref->getProperty($property); } catch (\ReflectionException $e) { throw new \BadMethodCallException($e->getMessage()); } - $property->setAccessible(true); - $property->setValue($this, $args[0]); + return $property; } } diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 2e5d1c34e507..b886d21ee6a2 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -77,6 +77,9 @@ public function __construct(array $config = []) 'customFilters' => [ 'commitTimestamp' => function ($v) { return $this->formatTimestampFromApi($v); + }, + 'readTimestamp' => function ($v) { + return $this->formatTimestampFromApi($v); } ] ]); @@ -422,9 +425,9 @@ public function beginTransaction(array $args = []) { $options = new TransactionOptions; - if (isset($args['readOnly'])) { + if (isset($args['transactionOptions']['readOnly'])) { $readOnly = (new TransactionOptions\ReadOnly) - ->deserialize($args['readOnly'], $this->codec); + ->deserialize($args['transactionOptions']['readOnly'], $this->codec); $options->setReadOnly($readOnly); } else { diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 3a1da137f6e7..3d531b381c51 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -378,7 +378,9 @@ public function readOnlyTransaction(array $options = []) } } - return $this->transaction(SessionPoolInterface::CONTEXT_READ, $options); + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + + return $this->operation->transaction($session, SessionPoolInterface::CONTEXT_READ, $options); } /** @@ -397,7 +399,9 @@ public function lockingTransaction(array $options = []) 'readWrite' => [] ]; - return $this->transaction(SessionPoolInterface::CONTEXT_READWRITE, $options); + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + return $this->operation->transaction($session, SessionPoolInterface::CONTEXT_READWRITE, $options); } /** diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 1a66253c1e00..c9f584ea7351 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -237,6 +237,36 @@ public function read(Session $session, $table, array $options = []) return $this->createResult($res); } + /** + * Create a transaction with a given context. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the transaction in. + * @param string $context The context of the new transaction. + * @param array $options [optional] Configuration options. + * @return Transaction + */ + public function transaction(Session $session, $context, array $options = []) + { + $options += [ + 'transactionOptions' => [] + ]; + + // make a service call here. + $res = $this->connection->beginTransaction($options + [ + 'session' => $session->name(), + 'context' => $context, + ]); + + $timestamp = null; + if (isset($res['readTimestamp'])) { + $timestamp = $this->mapper->createTimestampWithNanos($res['readTimestamp']); + } + + return new Transaction($this, $session, $context, $res['id'], $timestamp); + } + private function createResult(array $res) { $columns = $res['metadata']['rowType']['fields']; diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 3b70acbc89d9..04df34f74875 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -47,7 +47,7 @@ class Transaction private $transactionId; /** - * @var string + * @var Timestamp */ private $readTimestamp; @@ -60,21 +60,21 @@ class Transaction * @param Operation $operation The Operation instance. * @param Session $session The session to use for spanner interactions. * @param string $context The Transaction context. - * @param array $transaction Transaction details. + * @param string $transactionId The Transaction ID. + * @param Timestamp $readTimestamp [optional] The read timestamp. */ public function __construct( Operation $operation, Session $session, $context, - array $transaction + $transactionId, + Timestamp $readTimestamp = null ) { $this->operation = $operation; $this->session = $session; $this->context = $context; - $this->transactionId = $transaction['id']; - $this->readTimestamp = (isset($transaction['readTimestamp'])) - ? $transaction['readTimestamp'] - : null; + $this->transactionId = $transactionId; + $this->readTimestamp = $readTimestamp; } /** @@ -200,7 +200,7 @@ public function delete($table, KeySet $keySet) * * Example: * ``` - * $result = $spanner->execute( + * $result = $transaction->execute( * 'SELECT * FROM Users WHERE id = @userId', * [ * 'parameters' => [ @@ -290,6 +290,19 @@ public function rollback(array $options = []) return $this->operation->rollback($this->session, $this->transactionId, $options); } + /** + * Retrieve the Read Timestamp. + * + * For snapshot read-only transactions, the read timestamp chosen for the + * transaction. + * + * @return Timestamp + */ + public function readTimestamp() + { + return $this->readTimestamp; + } + /** * Format, validate and enqueue mutations in the transaction. * diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php new file mode 100644 index 000000000000..f9e22fa10591 --- /dev/null +++ b/tests/unit/Spanner/TransactionTest.php @@ -0,0 +1,332 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->operation = new Operation($this->connection->reveal(), false); + + $this->session = new Session( + $this->connection->reveal(), + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + ); + + $args = [ + $this->operation, + $this->session, + null, + self::TRANSACTION, + ]; + + $props = [ + 'operation' + ]; + + $this->transactionCallable = function ($context, $ts = null) use ($args, $props) { + $args[2] = $context; + + if (!is_null($ts)) { + $args[] = new Timestamp(new \DateTimeImmutable($ts)); + } + + $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props); + }; + + call_user_func_array($this->transactionCallable, [SessionPoolInterface::CONTEXT_READWRITE]); + } + + public function testInsert() + { + $this->transaction->insert('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + } + + public function testInsertBatch() + { + $this->transaction->insertBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + } + + public function testUpdate() + { + $this->transaction->update('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['update']['table']); + $this->assertEquals('foo', $mutations[0]['update']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['update']['values'][0]); + } + + public function testUpdateBatch() + { + $this->transaction->updateBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['update']['table']); + $this->assertEquals('foo', $mutations[0]['update']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['update']['values'][0]); + } + + public function testInsertOrUpdate() + { + $this->transaction->insertOrUpdate('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insertOrUpdate']['table']); + $this->assertEquals('foo', $mutations[0]['insertOrUpdate']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insertOrUpdate']['values'][0]); + } + + public function testInsertOrUpdateBatch() + { + $this->transaction->insertOrUpdateBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insertOrUpdate']['table']); + $this->assertEquals('foo', $mutations[0]['insertOrUpdate']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insertOrUpdate']['values'][0]); + } + + public function testReplace() + { + $this->transaction->replace('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['replace']['table']); + $this->assertEquals('foo', $mutations[0]['replace']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['replace']['values'][0]); + } + + public function testReplaceBatch() + { + $this->transaction->replaceBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['replace']['table']); + $this->assertEquals('foo', $mutations[0]['replace']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['replace']['values'][0]); + } + + public function testDelete() + { + $this->transaction->delete('Posts', new KeySet(['keys' => ['foo']])); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertEquals('Posts', $mutations[0]['delete']['table']); + $this->assertEquals('foo', $mutations[0]['delete']['keySet']['keys'][0]); + $this->assertFalse($mutations[0]['delete']['keySet']['all']); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Table'; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { + if ($arg['transactionId'] !== self::TRANSACTION) return false; + if ($arg['sql'] !== $sql) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->transaction->execute($sql); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $table = 'Table'; + $opts = ['foo' => 'bar']; + + $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { + if ($arg['transactionId'] !== self::TRANSACTION) return false; + if ($arg['table'] !== $table) return false; + if ($arg['foo'] !== $opts['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->transaction->read($table, $opts); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testCommit() + { + $this->transaction->insert('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $operation = $this->prophesize(Operation::class); + $operation->commit($this->session, $mutations, ['transactionId' => self::TRANSACTION])->shouldBeCalled(); + + $this->transaction->setOperation($operation->reveal()); + + $this->transaction->commit(); + } + + /** + * @expectedException RuntimeException + */ + public function testCommitInvalidContext() + { + call_user_func_array($this->transactionCallable, [SessionPoolInterface::CONTEXT_READ]); + + $this->transaction->commit(); + } + + /** + * @expectedException RuntimeException + */ + public function testEnqueueInvalidContext() + { + call_user_func_array($this->transactionCallable, [SessionPoolInterface::CONTEXT_READ]); + + $this->transaction->insert('Posts', []); + } + + public function testRollback() + { + $this->connection->rollback(Argument::any()) + ->shouldBeCalled(); + + $this->refreshOperation(); + + $this->transaction->rollback(); + } + + public function testReadTimestamp() + { + call_user_func_array($this->transactionCallable, [SessionPoolInterface::CONTEXT_READ, self::TIMESTAMP]); + + $ts = $this->transaction->readTimestamp(); + + $this->assertInstanceOf(Timestamp::class, $ts); + } + + // ******* + // Helpers + + private function refreshOperation() + { + $operation = new Operation($this->connection->reveal(), false); + $this->transaction->setOperation($operation); + } + + private function commitResponse() + { + return ['commitTimestamp' => self::TIMESTAMP]; + } + + private function assertTimestampIsCorrect($res) + { + $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']); + + $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z')); + } +} From 27a2f33932d3a712e95a52fb09116ac9b4792129 Mon Sep 17 00:00:00 2001 From: Michael Bausor Date: Tue, 10 Jan 2017 14:15:58 -0800 Subject: [PATCH 030/107] Regenerate spanner --- .../Admin/Database/V1/DatabaseAdminClient.php | 171 ++++++++++------ .../Admin/Instance/V1/InstanceAdminClient.php | 189 ++++++++++++------ src/Spanner/V1/SpannerClient.php | 99 ++++----- 3 files changed, 283 insertions(+), 176 deletions(-) diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php index 2632057f67c6..47ac7e8bf9c6 100644 --- a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php @@ -1,16 +1,18 @@ listDatabases($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $databaseAdminClient->close(); * } * ``` * @@ -95,8 +109,15 @@ class DatabaseAdminClient */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _CODEGEN_NAME = 'gapic'; - const _CODEGEN_VERSION = '0.1.0'; + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; private static $instanceNameTemplate; private static $databaseNameTemplate; @@ -106,6 +127,7 @@ class DatabaseAdminClient private $scopes; private $defaultCallSettings; private $descriptors; + private $operationsClient; /** * Formats a string containing the fully-qualified path to represent @@ -212,6 +234,25 @@ private static function getPageStreamingDescriptors() return $pageStreamingDescriptors; } + private static function getLongRunningDescriptors() + { + return [ + 'createDatabase' => [ + 'operationReturnType' => '\google\spanner\admin\database\v1\Database', + 'metadataReturnType' => '\google\spanner\admin\database\v1\CreateDatabaseMetadata', + ], + 'updateDatabaseDdl' => [ + 'operationReturnType' => '\google\protobuf\EmptyC', + 'metadataReturnType' => '\google\spanner\admin\database\v1\UpdateDatabaseDdlMetadata', + ], + ]; + } + + public function getOperationsClient() + { + return $this->operationsClient; + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -222,10 +263,10 @@ private static function getPageStreamingDescriptors() * @type string $serviceAddress The domain name of the API remote host. * Default 'wrenchworks.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. - * @type Grpc\ChannelCredentials $sslCreds + * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from - * Grpc\ChannelCredentials::createSsl() + * \Grpc\ChannelCredentials::createSsl() * @type array $scopes A string array of scopes to use when acquiring credentials. * Default the scopes for the Google Cloud Spanner Admin Database API. * @type array $retryingOverride @@ -240,21 +281,20 @@ private static function getPageStreamingDescriptors() * @type string $appName The codename of the calling service. Default 'gax'. * @type string $appVersion The version of the calling service. * Default: the current version of GAX. - * @type Google\Auth\CredentialsLoader $credentialsLoader + * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. * } */ public function __construct($options = []) { - $defaultScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.admin', - ]; $defaultOptions = [ 'serviceAddress' => self::SERVICE_ADDRESS, 'port' => self::DEFAULT_SERVICE_PORT, - 'scopes' => $defaultScopes, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', @@ -262,11 +302,16 @@ public function __construct($options = []) ]; $options = array_merge($defaultOptions, $options); + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + ]); + $headerDescriptor = new AgentHeaderDescriptor([ 'clientName' => $options['appName'], 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::_CODEGEN_NAME, - 'codeGenVersion' => self::_CODEGEN_VERSION, + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -287,6 +332,10 @@ public function __construct($options = []) foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; } + $longRunningDescriptors = self::getLongRunningDescriptors(); + foreach ($longRunningDescriptors as $method => $longRunningDescriptor) { + $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient]; + } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/database_admin_client_config.json'); $clientConfig = json_decode($clientConfigJsonString, true); @@ -311,6 +360,9 @@ public function __construct($options = []) $createDatabaseAdminStubFunction = function ($hostname, $opts) { return new DatabaseAdminGrpcClient($hostname, $opts); }; + if (array_key_exists('createDatabaseAdminStubFunction', $options)) { + $createDatabaseAdminStubFunction = $options['createDatabaseAdminStubFunction']; + } $this->databaseAdminStub = $this->grpcCredentialsHelper->createStub( $createDatabaseAdminStubFunction, $options['serviceAddress'], @@ -327,13 +379,21 @@ public function __construct($options = []) * try { * $databaseAdminClient = new DatabaseAdminClient(); * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * foreach ($databaseAdminClient->listDatabases($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $databaseAdminClient->close(); * } * ``` * @@ -406,11 +466,17 @@ public function listDatabases($parent, $optionalArgs = []) * $databaseAdminClient = new DatabaseAdminClient(); * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $createStatement = ""; - * $response = $databaseAdminClient->createDatabase($formattedParent, $createStatement); - * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); + * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) * } + * } finally { + * $databaseAdminClient->close(); * } * ``` * @@ -476,9 +542,7 @@ public function createDatabase($parent, $createStatement, $optionalArgs = []) * $formattedName = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $response = $databaseAdminClient->getDatabase($formattedName); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -535,11 +599,16 @@ public function getDatabase($name, $optionalArgs = []) * $databaseAdminClient = new DatabaseAdminClient(); * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $statements = []; - * $response = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); - * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); + * $operationResponse = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * // operation succeeded and returns no value + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) * } + * } finally { + * $databaseAdminClient->close(); * } * ``` * @@ -617,9 +686,7 @@ public function updateDatabaseDdl($database, $statements, $optionalArgs = []) * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $databaseAdminClient->dropDatabase($formattedDatabase); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -670,9 +737,7 @@ public function dropDatabase($database, $optionalArgs = []) * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $response = $databaseAdminClient->getDatabaseDdl($formattedDatabase); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -728,9 +793,7 @@ public function getDatabaseDdl($database, $optionalArgs = []) * $policy = new Policy(); * $response = $databaseAdminClient->setIamPolicy($formattedResource, $policy); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -792,9 +855,7 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $response = $databaseAdminClient->getIamPolicy($formattedResource); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -853,9 +914,7 @@ public function getIamPolicy($resource, $optionalArgs = []) * $permissions = []; * $response = $databaseAdminClient->testIamPermissions($formattedResource, $permissions); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php index e60ef9b14714..1346b8d3d9db 100644 --- a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -1,16 +1,18 @@ listInstanceConfigs($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -114,8 +127,15 @@ class InstanceAdminClient */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _CODEGEN_NAME = 'gapic'; - const _CODEGEN_VERSION = '0.1.0'; + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; private static $projectNameTemplate; private static $instanceConfigNameTemplate; @@ -126,6 +146,7 @@ class InstanceAdminClient private $scopes; private $defaultCallSettings; private $descriptors; + private $operationsClient; /** * Formats a string containing the fully-qualified path to represent @@ -259,6 +280,25 @@ private static function getPageStreamingDescriptors() return $pageStreamingDescriptors; } + private static function getLongRunningDescriptors() + { + return [ + 'createInstance' => [ + 'operationReturnType' => '\google\spanner\admin\instance\v1\Instance', + 'metadataReturnType' => '\google\spanner\admin\instance\v1\CreateInstanceMetadata', + ], + 'updateInstance' => [ + 'operationReturnType' => '\google\spanner\admin\instance\v1\Instance', + 'metadataReturnType' => '\google\spanner\admin\instance\v1\UpdateInstanceMetadata', + ], + ]; + } + + public function getOperationsClient() + { + return $this->operationsClient; + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -269,10 +309,10 @@ private static function getPageStreamingDescriptors() * @type string $serviceAddress The domain name of the API remote host. * Default 'wrenchworks.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. - * @type Grpc\ChannelCredentials $sslCreds + * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from - * Grpc\ChannelCredentials::createSsl() + * \Grpc\ChannelCredentials::createSsl() * @type array $scopes A string array of scopes to use when acquiring credentials. * Default the scopes for the Google Cloud Spanner Admin Instance API. * @type array $retryingOverride @@ -287,21 +327,20 @@ private static function getPageStreamingDescriptors() * @type string $appName The codename of the calling service. Default 'gax'. * @type string $appVersion The version of the calling service. * Default: the current version of GAX. - * @type Google\Auth\CredentialsLoader $credentialsLoader + * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. * } */ public function __construct($options = []) { - $defaultScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.admin', - ]; $defaultOptions = [ 'serviceAddress' => self::SERVICE_ADDRESS, 'port' => self::DEFAULT_SERVICE_PORT, - 'scopes' => $defaultScopes, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', @@ -309,11 +348,16 @@ public function __construct($options = []) ]; $options = array_merge($defaultOptions, $options); + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + ]); + $headerDescriptor = new AgentHeaderDescriptor([ 'clientName' => $options['appName'], 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::_CODEGEN_NAME, - 'codeGenVersion' => self::_CODEGEN_VERSION, + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -335,6 +379,10 @@ public function __construct($options = []) foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; } + $longRunningDescriptors = self::getLongRunningDescriptors(); + foreach ($longRunningDescriptors as $method => $longRunningDescriptor) { + $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient]; + } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/instance_admin_client_config.json'); $clientConfig = json_decode($clientConfigJsonString, true); @@ -359,6 +407,9 @@ public function __construct($options = []) $createInstanceAdminStubFunction = function ($hostname, $opts) { return new InstanceAdminGrpcClient($hostname, $opts); }; + if (array_key_exists('createInstanceAdminStubFunction', $options)) { + $createInstanceAdminStubFunction = $options['createInstanceAdminStubFunction']; + } $this->instanceAdminStub = $this->grpcCredentialsHelper->createStub( $createInstanceAdminStubFunction, $options['serviceAddress'], @@ -375,13 +426,21 @@ public function __construct($options = []) * try { * $instanceAdminClient = new InstanceAdminClient(); * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); - * foreach ($instanceAdminClient->listInstanceConfigs($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -449,9 +508,7 @@ public function listInstanceConfigs($parent, $optionalArgs = []) * $formattedName = InstanceAdminClient::formatInstanceConfigName("[PROJECT]", "[INSTANCE_CONFIG]"); * $response = $instanceAdminClient->getInstanceConfig($formattedName); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -501,13 +558,21 @@ public function getInstanceConfig($name, $optionalArgs = []) * try { * $instanceAdminClient = new InstanceAdminClient(); * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); - * foreach ($instanceAdminClient->listInstances($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $instanceAdminClient->listInstances($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstances($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -595,9 +660,7 @@ public function listInstances($parent, $optionalArgs = []) * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $response = $instanceAdminClient->getInstance($formattedName); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -682,11 +745,17 @@ public function getInstance($name, $optionalArgs = []) * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); * $instanceId = ""; * $instance = new Instance(); - * $response = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * $operationResponse = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -783,11 +852,17 @@ public function createInstance($parent, $instanceId, $instance, $optionalArgs = * $instanceAdminClient = new InstanceAdminClient(); * $instance = new Instance(); * $fieldMask = new FieldMask(); - * $response = $instanceAdminClient->updateInstance($instance, $fieldMask); - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * $operationResponse = $instanceAdminClient->updateInstance($instance, $fieldMask); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -854,9 +929,7 @@ public function updateInstance($instance, $fieldMask, $optionalArgs = []) * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $instanceAdminClient->deleteInstance($formattedName); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -911,9 +984,7 @@ public function deleteInstance($name, $optionalArgs = []) * $policy = new Policy(); * $response = $instanceAdminClient->setIamPolicy($formattedResource, $policy); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -975,9 +1046,7 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $response = $instanceAdminClient->getIamPolicy($formattedResource); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -1036,9 +1105,7 @@ public function getIamPolicy($resource, $optionalArgs = []) * $permissions = []; * $response = $instanceAdminClient->testIamPermissions($formattedResource, $permissions); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index 45c6bbb66631..f5ce68ac1e16 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -1,16 +1,18 @@ createSession($formattedDatabase); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -96,8 +96,15 @@ class SpannerClient */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _CODEGEN_NAME = 'gapic'; - const _CODEGEN_VERSION = '0.1.0'; + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; private static $databaseNameTemplate; private static $sessionNameTemplate; @@ -216,14 +223,6 @@ private static function getSessionNameTemplate() return self::$sessionNameTemplate; } - private static function getPageStreamingDescriptors() - { - $pageStreamingDescriptors = [ - ]; - - return $pageStreamingDescriptors; - } - // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -234,10 +233,10 @@ private static function getPageStreamingDescriptors() * @type string $serviceAddress The domain name of the API remote host. * Default 'wrenchworks.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. - * @type Grpc\ChannelCredentials $sslCreds + * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from - * Grpc\ChannelCredentials::createSsl() + * \Grpc\ChannelCredentials::createSsl() * @type array $scopes A string array of scopes to use when acquiring credentials. * Default the scopes for the Google Cloud Spanner API. * @type array $retryingOverride @@ -252,21 +251,20 @@ private static function getPageStreamingDescriptors() * @type string $appName The codename of the calling service. Default 'gax'. * @type string $appVersion The version of the calling service. * Default: the current version of GAX. - * @type Google\Auth\CredentialsLoader $credentialsLoader + * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. * } */ public function __construct($options = []) { - $defaultScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.data', - ]; $defaultOptions = [ 'serviceAddress' => self::SERVICE_ADDRESS, 'port' => self::DEFAULT_SERVICE_PORT, - 'scopes' => $defaultScopes, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.data', + ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', @@ -277,8 +275,8 @@ public function __construct($options = []) $headerDescriptor = new AgentHeaderDescriptor([ 'clientName' => $options['appName'], 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::_CODEGEN_NAME, - 'codeGenVersion' => self::_CODEGEN_VERSION, + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -294,10 +292,6 @@ public function __construct($options = []) 'commit' => $defaultDescriptors, 'rollback' => $defaultDescriptors, ]; - $pageStreamingDescriptors = self::getPageStreamingDescriptors(); - foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { - $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; - } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/spanner_client_config.json'); $clientConfig = json_decode($clientConfigJsonString, true); @@ -322,6 +316,9 @@ public function __construct($options = []) $createSpannerStubFunction = function ($hostname, $opts) { return new SpannerGrpcClient($hostname, $opts); }; + if (array_key_exists('createSpannerStubFunction', $options)) { + $createSpannerStubFunction = $options['createSpannerStubFunction']; + } $this->spannerStub = $this->grpcCredentialsHelper->createStub( $createSpannerStubFunction, $options['serviceAddress'], @@ -359,9 +356,7 @@ public function __construct($options = []) * $formattedDatabase = SpannerClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $response = $spannerClient->createSession($formattedDatabase); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -414,9 +409,7 @@ public function createSession($database, $optionalArgs = []) * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $response = $spannerClient->getSession($formattedName); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -467,9 +460,7 @@ public function getSession($name, $optionalArgs = []) * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $spannerClient->deleteSession($formattedName); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -529,9 +520,7 @@ public function deleteSession($name, $optionalArgs = []) * $sql = ""; * $response = $spannerClient->executeSql($formattedSession, $sql); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -653,9 +642,7 @@ public function executeSql($session, $sql, $optionalArgs = []) * $keySet = new KeySet(); * $response = $spannerClient->read($formattedSession, $table, $columns, $keySet); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -757,9 +744,7 @@ public function read($session, $table, $columns, $keySet, $optionalArgs = []) * $options = new TransactionOptions(); * $response = $spannerClient->beginTransaction($formattedSession, $options); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -820,9 +805,7 @@ public function beginTransaction($session, $options, $optionalArgs = []) * $mutations = []; * $response = $spannerClient->commit($formattedSession, $mutations); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -905,9 +888,7 @@ public function commit($session, $mutations, $optionalArgs = []) * $transactionId = ""; * $spannerClient->rollback($formattedSession, $transactionId); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * From 0a159a5b6e86284eafdc56d75dd6fe71687a5856 Mon Sep 17 00:00:00 2001 From: Michael Bausor Date: Tue, 10 Jan 2017 14:28:27 -0800 Subject: [PATCH 031/107] Include config changes and comment updates --- .../V1/resources/database_admin_client_config.json | 4 ++-- .../Admin/Instance/V1/InstanceAdminClient.php | 12 +++++++----- .../V1/resources/instance_admin_client_config.json | 4 ++-- src/Spanner/V1/resources/spanner_client_config.json | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json index 16f75e93befb..efa919a0a7d8 100644 --- a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json +++ b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json @@ -12,9 +12,9 @@ }, "retry_params": { "default": { - "initial_retry_delay_millis": 100, + "initial_retry_delay_millis": 1000, "retry_delay_multiplier": 1.3, - "max_retry_delay_millis": 60000, + "max_retry_delay_millis": 32000, "initial_rpc_timeout_millis": 60000, "rpc_timeout_multiplier": 1.0, "max_rpc_timeout_millis": 60000, diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php index 1346b8d3d9db..a47467fdb9c7 100644 --- a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -601,13 +601,15 @@ public function getInstanceConfig($name, $optionalArgs = []) * Some examples of using filters are: * * * name:* --> The instance has a name. - * * name:Howl --> The instance's name is howl. + * * 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's label env has the value dev. - * * name:howl labels.env:dev --> The instance's name is howl and it has - * the label env with value dev. + * * labels.env:* --> The instance has the label "env". + * * labels.env:dev --> The instance has the label "env" and the value of + * the label contains the string "dev". + * * name:howl labels.env:dev --> The instance's name contains "howl" and + * it has the label "env" with its value + * containing "dev". * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. diff --git a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json index 6771a7e9d440..23dbca4fe655 100644 --- a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json +++ b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json @@ -12,9 +12,9 @@ }, "retry_params": { "default": { - "initial_retry_delay_millis": 100, + "initial_retry_delay_millis": 1000, "retry_delay_multiplier": 1.3, - "max_retry_delay_millis": 60000, + "max_retry_delay_millis": 32000, "initial_rpc_timeout_millis": 60000, "rpc_timeout_multiplier": 1.0, "max_rpc_timeout_millis": 60000, diff --git a/src/Spanner/V1/resources/spanner_client_config.json b/src/Spanner/V1/resources/spanner_client_config.json index 6299ccfa6961..db4ced68c440 100644 --- a/src/Spanner/V1/resources/spanner_client_config.json +++ b/src/Spanner/V1/resources/spanner_client_config.json @@ -12,9 +12,9 @@ }, "retry_params": { "default": { - "initial_retry_delay_millis": 100, + "initial_retry_delay_millis": 1000, "retry_delay_multiplier": 1.3, - "max_retry_delay_millis": 60000, + "max_retry_delay_millis": 32000, "initial_rpc_timeout_millis": 60000, "rpc_timeout_multiplier": 1.0, "max_rpc_timeout_millis": 60000, From f2b772aa15091c793414b29c1e1383105d127e98 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 11 Jan 2017 09:49:27 -0500 Subject: [PATCH 032/107] Add operations tests --- dev/src/StubTrait.php | 12 ++ src/GrpcRequestWrapper.php | 2 +- src/Spanner/Connection/Grpc.php | 71 +++--- src/Spanner/Operation.php | 93 ++++---- src/Spanner/Transaction.php | 20 ++ src/Spanner/V1/SpannerClient.php | 2 +- tests/unit/Spanner/OperationTest.php | 286 +++++++++++++++++++++++++ tests/unit/Spanner/TransactionTest.php | 35 ++- 8 files changed, 422 insertions(+), 99 deletions(-) create mode 100644 tests/unit/Spanner/OperationTest.php diff --git a/dev/src/StubTrait.php b/dev/src/StubTrait.php index 8f0982680778..df55c4a5e1c0 100644 --- a/dev/src/StubTrait.php +++ b/dev/src/StubTrait.php @@ -27,6 +27,18 @@ public function ___getProperty($prop) return $property->getValue($this); } + public function ___setProperty($prop, $value) + { + if (!in_array($prop, json_decode($this->___props))) { + throw new \BadMethodCallException(sprintf('Property %s cannot be overloaded', $prop)); + } + + $property = $this->___getPropertyReflector($prop); + + $property->setAccessible(true); + $property->setValue($this, $value); + } + public function __call($method, $args) { $matches = []; diff --git a/src/GrpcRequestWrapper.php b/src/GrpcRequestWrapper.php index f75c255ab71c..4d73e671f023 100644 --- a/src/GrpcRequestWrapper.php +++ b/src/GrpcRequestWrapper.php @@ -127,7 +127,7 @@ public function send(callable $request, array $args, array $options = []) try { return $this->handleResponse($backoff->execute($request, $args)); - } catch (\Exception $ex) { + } catch (ApiException $ex) { throw $this->convertToGoogleException($ex); } } diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index b886d21ee6a2..280c6a071783 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -29,6 +29,7 @@ use google\spanner\admin\instance\v1\Instance; use google\spanner\admin\instance\v1\State; use google\spanner\v1; +use google\spanner\v1\KeySet; use google\spanner\v1\Mutation; use google\spanner\v1\TransactionOptions; use google\spanner\v1\Type; @@ -378,36 +379,9 @@ public function executeSql(array $args = []) */ public function read(array $args = []) { - $keys = $this->pluck('keySet', $args); - - $keySet = new v1\KeySet; - if (!empty($keys['keys'])) { - $keySet->setKeys($this->formatListForApi($keys['keys'])); - } - - if (!empty($keys['ranges'])) { - $ranges = new v1\KeyRange; - - if (isset($keys['ranges']['startClosed'])) { - $ranges->setStartClosed($this->formatListForApi($keys['ranges']['startClosed'])); - } - - if (isset($keys['ranges']['startOpen'])) { - $ranges->setStartOpen($this->formatListForApi($keys['ranges']['startOpen'])); - } - if (isset($keys['ranges']['endClosed'])) { - $ranges->setEndClosed($this->formatListForApi($keys['ranges']['endClosed'])); - } - if (isset($keys['ranges']['endOpen'])) { - $ranges->setEndOpen($this->formatListForApi($keys['ranges']['endOpen'])); - } - - $keySet->setRanges($ranges); - } - - if (isset($keys['all'])) { - $keySet->setAll($keys['all']); - } + $keySet = $this->pluck('keySet', $args); + $keySet = (new KeySet) + ->deserialize($this->formatKeySet($keySet), $this->codec); return $this->send([$this->spannerClient, 'read'], [ $this->pluck('session', $args), @@ -468,18 +442,8 @@ public function commit(array $args = []) break; case 'delete': - if (isset($data['keySet']['keys'])) { - $data['keySet']['keys'] = $this->formatListForApi($data['keySet']['keys']); - } - - if (isset($data['keySet']['ranges'])) { - foreach ($data['keySet']['ranges'] as $index => $rangeItem) { - foreach ($rangeItem as $key => $val) { - $rangeItem[$key] = $this->formatListForApi($val); - } - - $data['keySet']['ranges'][$index] = $rangeItem; - } + if (isset($data['keySet'])) { + $data['keySet'] = $this->formatKeySet($data['keySet']); } $operation = (new Mutation\Delete) @@ -522,4 +486,27 @@ public function rollback(array $args = []) $args ]); } + + /** + * @param array $keySet + * @return array Formatted keyset + */ + private function formatKeySet(array $keySet) + { + if (isset($keySet['keys'])) { + $keySet['keys'] = $this->formatListForApi($keySet['keys']); + } + + if (isset($keySet['ranges'])) { + foreach ($keySet['ranges'] as $index => $rangeItem) { + foreach ($rangeItem as $key => $val) { + $rangeItem[$key] = $this->formatListForApi($val); + } + + $keySet['ranges'][$index] = $rangeItem; + } + } + + return $keySet; + } } diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index c9f584ea7351..931986188acb 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -21,13 +21,16 @@ use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\ValidateTrait; -use RuntimeException; /** * Common interface for running operations against Google Cloud Spanner. This * class is intended for internal use by the client library only. Implementors * should access these operations via {@see Google\Cloud\Spanner\Database} or * {@see Google\Cloud\Spanner\Transaction}. + * + * Usage examples may be found in classes making use of this class: + * * {@see Google\Cloud\Spanner\Database} + * * {@see Google\Cloud\Spanner\Transaction} */ class Operation { @@ -74,9 +77,7 @@ public function __construct(ConnectionInterface $connection, $returnInt64AsObjec */ public function mutation($operation, $table, $mutation) { - $mutation = array_filter($mutation, function ($value) { - return !is_null($value); - }); + $mutation = $this->arrayFilterPreserveBool($mutation); return [ $operation => [ @@ -96,33 +97,10 @@ public function mutation($operation, $table, $mutation) */ public function deleteMutation($table, KeySet $keySet) { - $keyRanges = $keySet->ranges(); - if ($keyRanges) { - $ranges = []; - foreach ($keyRanges as $range) { - $types = $range->types(); - - $start = $range->start(); - $range->setStart($types['start'], $this->mapper->encodeValuesAsSimpleType($start)); - - $end = $range->end(); - $range->setEnd($types['end'], $this->mapper->encodeValuesAsSimpleType($end)); - - $ranges[] = $range; - } - - $keySet->setRanges($ranges); - } - - $keys = $keySet->keySetObject(); - if (!empty($keys['keys'])) { - $keys['keys'] = $this->mapper->encodeValuesAsSimpleType($keys['keys']); - } - return [ self::OP_DELETE => [ 'table' => $table, - 'keySet' => $this->arrayFilterPreserveBool($keys) + 'keySet' => $this->flattenKeySet($keySet), ] ]; } @@ -213,21 +191,19 @@ public function read(Session $session, $table, array $options = []) $options += [ 'index' => null, 'columns' => [], - 'keySet' => [], + 'keySet' => null, 'offset' => null, 'limit' => null, ]; - if (!empty($options['keySet']) && !($options['keySet']) instanceof KeySet) { - throw new RuntimeException('$options.keySet must be an instance of KeySet'); - } - - if (empty($options['keySet'])) { + if (is_null($options['keySet'])) { $options['keySet'] = new KeySet(); $options['keySet']->setMatchAll(true); + } elseif (!($options['keySet'] instanceof KeySet)) { + throw new \InvalidArgumentException('$options.keySet must be an instance of KeySet'); } - $options['keySet'] = $options['keySet']->keySetObject(); + $options['keySet'] = $this->flattenKeySet($options['keySet']); $res = $this->connection->read([ 'table' => $table, @@ -256,7 +232,6 @@ public function transaction(Session $session, $context, array $options = []) // make a service call here. $res = $this->connection->beginTransaction($options + [ 'session' => $session->name(), - 'context' => $context, ]); $timestamp = null; @@ -267,9 +242,19 @@ public function transaction(Session $session, $context, array $options = []) return new Transaction($this, $session, $context, $res['id'], $timestamp); } + /** + * Transform a service read or executeSql response to a friendly result. + * + * @codingStandardsIgnoreStart + * @param array $res [ResultSet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet) + * @codingStandardsIgnoreEnd + * @return Result + */ private function createResult(array $res) { - $columns = $res['metadata']['rowType']['fields']; + $columns = isset($res['metadata']['rowType']['fields']) + ? $res['metadata']['rowType']['fields'] + : []; $rows = []; if (isset($res['rows'])) { @@ -281,6 +266,40 @@ private function createResult(array $res) return new Result($res, $rows); } + /** + * Convert a KeySet object to an API-ready array. + * + * @param KeySet $keySet The keySet object. + * @return array [KeySet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#keyset) + */ + private function flattenKeySet(KeySet $keySet) + { + $keyRanges = $keySet->ranges(); + if ($keyRanges) { + $ranges = []; + foreach ($keyRanges as $range) { + $types = $range->types(); + + $start = $range->start(); + $range->setStart($types['start'], $this->mapper->encodeValuesAsSimpleType($start)); + + $end = $range->end(); + $range->setEnd($types['end'], $this->mapper->encodeValuesAsSimpleType($end)); + + $ranges[] = $range; + } + + $keySet->setRanges($ranges); + } + + $keys = $keySet->keySetObject(); + if (!empty($keys['keys'])) { + $keys['keys'] = $this->mapper->encodeValuesAsSimpleType($keys['keys']); + } + + return $this->arrayFilterPreserveBool($keys); + } + /** * Represent the class in a more readable and digestable fashion. * diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 04df34f74875..8ed22cdef6cb 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -303,6 +303,26 @@ public function readTimestamp() return $this->readTimestamp; } + /** + * Retrieve the Transaction ID. + * + * @return string + */ + public function id() + { + return $this->transactionId; + } + + /** + * Retrieve the Transaction Context + * + * @return string + */ + public function context() + { + return $this->context; + } + /** * Format, validate and enqueue mutations in the transaction. * diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index e39d23144ff6..45c6bbb66631 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -880,7 +880,7 @@ public function commit($session, $mutations, $optionalArgs = []) $mergedSettings, $this->descriptors['commit'] ); -// print_r($request->serialize(new \Google\Cloud\PhpArray));exit; + return $callable( $request, [], diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php new file mode 100644 index 000000000000..7443ddeefada --- /dev/null +++ b/tests/unit/Spanner/OperationTest.php @@ -0,0 +1,286 @@ +connection = $this->prophesize(ConnectionInterface::class); + + $this->operation = \Google\Cloud\Dev\stub(Operation::class, [ + $this->connection->reveal(), + false + ]); + + $session = $this->prophesize(Session::class); + $session->name()->willReturn(self::SESSION); + $this->session = $session->reveal(); + } + + public function testMutation() + { + $res = $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]); + + $this->assertEquals(Operation::OP_INSERT, array_keys($res)[0]); + $this->assertEquals('Posts', $res[Operation::OP_INSERT]['table']); + $this->assertEquals('foo', $res[Operation::OP_INSERT]['columns'][0]); + $this->assertEquals('bar', $res[Operation::OP_INSERT]['values'][0]); + } + + public function testDeleteMutation() + { + $keys = ['foo', 'bar']; + $range = new KeyRange([ + 'startType' => KeyRange::TYPE_CLOSED, + 'start' => ['foo'], + 'endType' => KeyRange::TYPE_OPEN, + 'end' => ['bar'] + ]); + + $keySet = new KeySet([ + 'keys' => $keys, + 'ranges' => [$range] + ]); + + $res = $this->operation->deleteMutation('Posts', $keySet); + + $this->assertEquals('Posts', $res['delete']['table']); + $this->assertEquals($keys, $res['delete']['keySet']['keys']); + $this->assertEquals($range->keyRangeObject(), $res['delete']['keySet']['ranges'][0]); + } + + public function testCommit() + { + $mutations = [ + $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]) + ]; + + $this->connection->commit(Argument::that(function ($arg) use ($mutations) { + if ($arg['mutations'] !== $mutations) return false; + if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + + return true; + }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); + + $this->operation->setConnection($this->connection->reveal()); + + $res = $this->operation->commit($this->session, $mutations); + + $this->assertInstanceOf(Timestamp::class, $res); + } + + public function testCommitWithExistingTransaction() + { + $mutations = [ + $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]) + ]; + + $this->connection->commit(Argument::that(function ($arg) use ($mutations) { + if ($arg['mutations'] !== $mutations) return false; + if (isset($arg['singleUseTransaction'])) return false; + if ($arg['transactionId'] !== self::TRANSACTION) return false; + + return true; + }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); + + $this->operation->setConnection($this->connection->reveal()); + + $res = $this->operation->commit($this->session, $mutations, [ + 'transactionId' => self::TRANSACTION + ]); + + $this->assertInstanceOf(Timestamp::class, $res); + } + + public function testRollback() + { + $this->connection->rollback(Argument::that(function ($arg) { + if ($arg['transactionId'] !== self::TRANSACTION) return false; + if ($arg['session'] !== self::SESSION) return false; + + return true; + }))->shouldBeCalled(); + + $this->operation->setConnection($this->connection->reveal()); + + $this->operation->rollback($this->session, self::TRANSACTION); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Posts WHERE ID = @id'; + $params = ['id' => 10]; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql, $params) { + if ($arg['sql'] !== $sql) return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['params'] !== ['id' => '10']) return false; + if ($arg['paramTypes']['id']['code'] !== ValueMapper::TYPE_INT64) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); + + $this->operation->setConnection($this->connection->reveal()); + + $res = $this->operation->execute($this->session, $sql, [ + 'parameters' => $params + ]); + + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); + + $this->operation->setConnection($this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts'); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testReadWithKeySet() + { + $keys = ['foo','bar']; + + $this->connection->read(Argument::that(function ($arg) use ($keys) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] === true) return false; + if ($arg['keySet']['keys'] !== $keys) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); + + $this->operation->setConnection($this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', [ + 'keySet' => new KeySet(['keys' => $keys]) + ]); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testReadWithInvalidKeySet() + { + $this->operation->read($this->session, 'Posts', [ + 'keySet' => 'foo' + ]); + } + + public function testTransaction() + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if ($arg['session'] !== self::SESSION) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'id' => self::TRANSACTION + ]); + + $this->operation->setConnection($this->connection->reveal()); + + $res = $this->operation->transaction($this->session, SessionPoolInterface::CONTEXT_READWRITE); + + $this->assertInstanceOf(Transaction::class, $res); + $this->assertEquals(self::TRANSACTION, $res->id()); + $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $res->context()); + $this->assertNull($res->readTimestamp()); + } + + public function testTransactionWithTimestamp() + { + $this->connection->beginTransaction(Argument::that(function ($arg) { + if ($arg['session'] !== self::SESSION) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'id' => self::TRANSACTION, + 'readTimestamp' => self::TIMESTAMP + ]); + + $this->operation->setConnection($this->connection->reveal()); + + $res = $this->operation->transaction($this->session, SessionPoolInterface::CONTEXT_READWRITE); + + $this->assertInstanceOf(Transaction::class, $res); + $this->assertInstanceOf(Timestamp::class, $res->readTimestamp()); + } + + private function executeAndReadResponse() + { + return [ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + ['10'] + ] + ]; + } +} diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php index f9e22fa10591..66be1b0adb14 100644 --- a/tests/unit/Spanner/TransactionTest.php +++ b/tests/unit/Spanner/TransactionTest.php @@ -63,25 +63,15 @@ public function setUp() $args = [ $this->operation, $this->session, - null, + SessionPoolInterface::CONTEXT_READWRITE, self::TRANSACTION, ]; $props = [ - 'operation' + 'operation', 'readTimestamp', 'context' ]; - $this->transactionCallable = function ($context, $ts = null) use ($args, $props) { - $args[2] = $context; - - if (!is_null($ts)) { - $args[] = new Timestamp(new \DateTimeImmutable($ts)); - } - - $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props); - }; - - call_user_func_array($this->transactionCallable, [SessionPoolInterface::CONTEXT_READWRITE]); + $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props); } public function testInsert() @@ -275,8 +265,7 @@ public function testCommit() */ public function testCommitInvalidContext() { - call_user_func_array($this->transactionCallable, [SessionPoolInterface::CONTEXT_READ]); - + $this->transaction->___setProperty('context', SessionPoolInterface::CONTEXT_READ); $this->transaction->commit(); } @@ -285,8 +274,7 @@ public function testCommitInvalidContext() */ public function testEnqueueInvalidContext() { - call_user_func_array($this->transactionCallable, [SessionPoolInterface::CONTEXT_READ]); - + $this->transaction->___setProperty('context', SessionPoolInterface::CONTEXT_READ); $this->transaction->insert('Posts', []); } @@ -302,13 +290,24 @@ public function testRollback() public function testReadTimestamp() { - call_user_func_array($this->transactionCallable, [SessionPoolInterface::CONTEXT_READ, self::TIMESTAMP]); + $this->transaction->___setProperty('context', SessionPoolInterface::CONTEXT_READ); + $this->transaction->___setProperty('readTimestamp', new Timestamp(new \DateTimeImmutable(self::TIMESTAMP))); $ts = $this->transaction->readTimestamp(); $this->assertInstanceOf(Timestamp::class, $ts); } + public function testId() + { + $this->assertEquals(self::TRANSACTION, $this->transaction->id()); + } + + public function testContext() + { + $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $this->transaction->context()); + } + // ******* // Helpers From 9b924408a1cfe2d746276ebaf9c140faa68c893b Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 11 Jan 2017 10:46:27 -0500 Subject: [PATCH 033/107] Update set connection calls --- dev/src/StubTrait.php | 19 -------------- tests/snippets/bootstrap.php | 2 +- tests/unit/Spanner/DatabaseTest.php | 8 +++--- tests/unit/Spanner/OperationTest.php | 16 ++++++------ tests/unit/Spanner/TransactionTest.php | 4 +-- tests/unit/SpannerAdmin/ConfigurationTest.php | 10 +++---- .../Connection/IamDatabaseTest.php | 6 ++--- .../Connection/IamInstanceTest.php | 6 ++--- tests/unit/SpannerAdmin/DatabaseTest.php | 16 ++++++------ tests/unit/SpannerAdmin/InstanceTest.php | 26 +++++++++---------- tests/unit/SpannerAdmin/SpannerClientTest.php | 10 +++---- 11 files changed, 52 insertions(+), 71 deletions(-) diff --git a/dev/src/StubTrait.php b/dev/src/StubTrait.php index df55c4a5e1c0..7a710ed8bb72 100644 --- a/dev/src/StubTrait.php +++ b/dev/src/StubTrait.php @@ -39,25 +39,6 @@ public function ___setProperty($prop, $value) $property->setValue($this, $value); } - public function __call($method, $args) - { - $matches = []; - if (!preg_match('/set([a-zA-z0-9]{0,})/', $method, $matches)) { - throw new \BadMethodCallException("Method $method does not exist"); - } - - $prop = lcfirst($matches[1]); - - if (!in_array($prop, json_decode($this->___props))) { - throw new \BadMethodCallException(sprintf('Property %s cannot be overloaded', $prop)); - } - - $property = $this->___getPropertyReflector($prop); - - $property->setAccessible(true); - $property->setValue($this, $args[0]); - } - private function ___getPropertyReflector($property) { $trait = new \ReflectionClass($this); diff --git a/tests/snippets/bootstrap.php b/tests/snippets/bootstrap.php index af9eec686e44..9e4a5da3d639 100644 --- a/tests/snippets/bootstrap.php +++ b/tests/snippets/bootstrap.php @@ -35,7 +35,7 @@ function stub($name, $extends) { - $tpl = 'class %s extends %s {use \Google\Cloud\Dev\SetStubConnectionTrait; }'; + $tpl = 'class %s extends %s {use \Google\Cloud\Dev\StubTrait; }'; eval(sprintf($tpl, $name, $extends)); } diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php index 4e038906dee5..f26d9c4a9314 100644 --- a/tests/unit/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/DatabaseTest.php @@ -89,7 +89,7 @@ public function testReadOnlyTransaction() 'id' => self::TRANSACTION ]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $t = $this->database->readOnlyTransaction(); $this->assertInstanceOf(Transaction::class, $t); @@ -121,7 +121,7 @@ public function testReadOnlyTransactionOptions() 'id' => self::TRANSACTION ]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->database->readOnlyTransaction($options); } @@ -146,7 +146,7 @@ public function testLockingTransaction() 'id' => self::TRANSACTION ]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $t = $this->database->lockingTransaction(); $this->assertInstanceOf(Transaction::class, $t); @@ -410,7 +410,7 @@ public function testRead() private function refreshOperation() { $operation = new Operation($this->connection->reveal(), false); - $this->database->setOperation($operation); + $this->database->___setProperty('operation', $operation); } private function commitResponse() diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php index 7443ddeefada..3f3ea0e29800 100644 --- a/tests/unit/Spanner/OperationTest.php +++ b/tests/unit/Spanner/OperationTest.php @@ -105,7 +105,7 @@ public function testCommit() return true; }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $this->operation->commit($this->session, $mutations); @@ -128,7 +128,7 @@ public function testCommitWithExistingTransaction() return true; }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $this->operation->commit($this->session, $mutations, [ 'transactionId' => self::TRANSACTION @@ -146,7 +146,7 @@ public function testRollback() return true; }))->shouldBeCalled(); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $this->operation->rollback($this->session, self::TRANSACTION); } @@ -165,7 +165,7 @@ public function testExecute() return true; }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $this->operation->execute($this->session, $sql, [ 'parameters' => $params @@ -185,7 +185,7 @@ public function testRead() return true; }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $this->operation->read($this->session, 'Posts'); $this->assertInstanceOf(Result::class, $res); @@ -205,7 +205,7 @@ public function testReadWithKeySet() return true; }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $this->operation->read($this->session, 'Posts', [ 'keySet' => new KeySet(['keys' => $keys]) @@ -234,7 +234,7 @@ public function testTransaction() 'id' => self::TRANSACTION ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $this->operation->transaction($this->session, SessionPoolInterface::CONTEXT_READWRITE); @@ -255,7 +255,7 @@ public function testTransactionWithTimestamp() 'readTimestamp' => self::TIMESTAMP ]); - $this->operation->setConnection($this->connection->reveal()); + $this->operation->___setProperty('connection', $this->connection->reveal()); $res = $this->operation->transaction($this->session, SessionPoolInterface::CONTEXT_READWRITE); diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php index 66be1b0adb14..400704f707ca 100644 --- a/tests/unit/Spanner/TransactionTest.php +++ b/tests/unit/Spanner/TransactionTest.php @@ -255,7 +255,7 @@ public function testCommit() $operation = $this->prophesize(Operation::class); $operation->commit($this->session, $mutations, ['transactionId' => self::TRANSACTION])->shouldBeCalled(); - $this->transaction->setOperation($operation->reveal()); + $this->transaction->___setProperty('operation', $operation->reveal()); $this->transaction->commit(); } @@ -314,7 +314,7 @@ public function testContext() private function refreshOperation() { $operation = new Operation($this->connection->reveal(), false); - $this->transaction->setOperation($operation); + $this->transaction->___setProperty('operation', $operation); } private function commitResponse() diff --git a/tests/unit/SpannerAdmin/ConfigurationTest.php b/tests/unit/SpannerAdmin/ConfigurationTest.php index 9ad7460446cc..360ba9e5e1c0 100644 --- a/tests/unit/SpannerAdmin/ConfigurationTest.php +++ b/tests/unit/SpannerAdmin/ConfigurationTest.php @@ -52,7 +52,7 @@ public function testName() public function testInfo() { $this->connection->getConfig(Argument::any())->shouldNotBeCalled(); - $this->configuration->setConnection($this->connection->reveal()); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $info = ['foo' => 'bar']; $config = \Google\Cloud\Dev\stub(Configuration::class, [ @@ -74,7 +74,7 @@ public function testInfoWithReload() 'projectId' => self::PROJECT_ID ])->shouldBeCalled()->willReturn($info); - $this->configuration->setConnection($this->connection->reveal()); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $this->assertEquals($info, $this->configuration->info()); } @@ -82,7 +82,7 @@ public function testInfoWithReload() public function testExists() { $this->connection->getConfig(Argument::any())->willReturn([]); - $this->configuration->setConnection($this->connection->reveal()); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $this->assertTrue($this->configuration->exists()); } @@ -90,7 +90,7 @@ public function testExists() public function testExistsDoesntExist() { $this->connection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404)); - $this->configuration->setConnection($this->connection->reveal()); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $this->assertFalse($this->configuration->exists()); } @@ -104,7 +104,7 @@ public function testReload() 'projectId' => self::PROJECT_ID ])->shouldBeCalledTimes(1)->willReturn($info); - $this->configuration->setConnection($this->connection->reveal()); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $info = $this->configuration->reload(); diff --git a/tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php b/tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php index edacd45a30e9..8138274b3204 100644 --- a/tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php +++ b/tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php @@ -46,7 +46,7 @@ public function testGetPolicy() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->getPolicy($args); @@ -62,7 +62,7 @@ public function testSetPolicy() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->setPolicy($args); @@ -78,7 +78,7 @@ public function testTestPermissions() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->testPermissions($args); diff --git a/tests/unit/SpannerAdmin/Connection/IamInstanceTest.php b/tests/unit/SpannerAdmin/Connection/IamInstanceTest.php index 6463a895071d..06ebffceed4f 100644 --- a/tests/unit/SpannerAdmin/Connection/IamInstanceTest.php +++ b/tests/unit/SpannerAdmin/Connection/IamInstanceTest.php @@ -46,7 +46,7 @@ public function testGetPolicy() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->getPolicy($args); @@ -62,7 +62,7 @@ public function testSetPolicy() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->setPolicy($args); @@ -78,7 +78,7 @@ public function testTestPermissions() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->testPermissions($args); diff --git a/tests/unit/SpannerAdmin/DatabaseTest.php b/tests/unit/SpannerAdmin/DatabaseTest.php index ff205ad0c52a..712c31b393d9 100644 --- a/tests/unit/SpannerAdmin/DatabaseTest.php +++ b/tests/unit/SpannerAdmin/DatabaseTest.php @@ -65,7 +65,7 @@ public function testExists() ->shouldBeCalled() ->willReturn([]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->assertTrue($this->database->exists()); } @@ -76,7 +76,7 @@ public function testExistsNotFound() ->shouldBeCalled() ->willThrow(new NotFoundException('', 404)); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->assertFalse($this->database->exists()); } @@ -89,7 +89,7 @@ public function testUpdateDdl() 'statements' => [$statement] ]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->database->updateDdl($statement); } @@ -102,7 +102,7 @@ public function testUpdateDdlBatch() 'statements' => $statements ]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->database->updateDdl($statements); } @@ -116,7 +116,7 @@ public function testUpdateWithSingleStatement() 'operationId' => null, ])->shouldBeCalled(); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->database->updateDdl($statement); } @@ -127,7 +127,7 @@ public function testDrop() 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->shouldBeCalled(); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->database->drop(); } @@ -139,7 +139,7 @@ public function testDdl() 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->willReturn(['statements' => $ddl]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->assertEquals($ddl, $this->database->ddl()); } @@ -150,7 +150,7 @@ public function testDdlNoResult() 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->willReturn([]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->assertEquals([], $this->database->ddl()); } diff --git a/tests/unit/SpannerAdmin/InstanceTest.php b/tests/unit/SpannerAdmin/InstanceTest.php index 9d38318f0a76..07e1eaf7a230 100644 --- a/tests/unit/SpannerAdmin/InstanceTest.php +++ b/tests/unit/SpannerAdmin/InstanceTest.php @@ -71,7 +71,7 @@ public function testInfoWithReload() ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $info = $this->instance->info(); $this->assertEquals('Instance Name', $info['displayName']); @@ -83,7 +83,7 @@ public function testExists() { $this->connection->getInstance(Argument::any())->shouldBeCalled()->willReturn([]); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->assertTrue($this->instance->exists()); } @@ -94,7 +94,7 @@ public function testExistsNotFound() ->shouldBeCalled() ->willThrow(new NotFoundException('foo', 404)); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->assertFalse($this->instance->exists()); } @@ -107,7 +107,7 @@ public function testReload() ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $info = $this->instance->reload(); @@ -122,7 +122,7 @@ public function testState() ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->assertEquals(Instance::STATE_READY, $this->instance->state()); } @@ -133,7 +133,7 @@ public function testStateIsNull() ->shouldBeCalledTimes(1) ->willReturn([]); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->assertNull($this->instance->state()); } @@ -153,7 +153,7 @@ public function testUpdate() 'labels' => [], ])->shouldBeCalled(); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->instance->update(); } @@ -174,7 +174,7 @@ public function testUpdateWithExistingLabels() 'labels' => $instance['labels'], ])->shouldBeCalled(); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->instance->update(); } @@ -202,7 +202,7 @@ public function testUpdateWithChanges() 'labels' => $changes['labels'], ])->shouldBeCalled(); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->instance->update($changes); } @@ -213,7 +213,7 @@ public function testDelete() 'name' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME) ])->shouldBeCalled(); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->instance->delete(); } @@ -234,7 +234,7 @@ public function testCreateDatabase() ->shouldBeCalled() ->willReturn($dbInfo); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $database = $this->instance->createDatabase('test-database', [ 'statements' => $extra @@ -262,7 +262,7 @@ public function testDatabases() ->shouldBeCalled() ->willReturn(['databases' => $databases]); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $dbs = $this->instance->databases(); @@ -287,7 +287,7 @@ public function testDatabasesPaged() ->shouldBeCalledTimes(2) ->willReturn(['databases' => [$databases[0]], 'nextPageToken' => 'foo'], ['databases' => [$databases[1]]]); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $dbs = $this->instance->databases(); diff --git a/tests/unit/SpannerAdmin/SpannerClientTest.php b/tests/unit/SpannerAdmin/SpannerClientTest.php index 493d3f7b1fe1..3bdd3a56616c 100644 --- a/tests/unit/SpannerAdmin/SpannerClientTest.php +++ b/tests/unit/SpannerAdmin/SpannerClientTest.php @@ -38,7 +38,7 @@ public function setUp() $this->connection = $this->prophesize(ConnectionInterface::class); $this->client = \Google\Cloud\Dev\stub(SpannerClient::class, [['projectId' => 'test-project']]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testConfigurations() @@ -57,7 +57,7 @@ public function testConfigurations() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $configs = $this->client->configurations(); @@ -94,7 +94,7 @@ public function testPagedConfigurations() ->shouldBeCalledTimes(2) ->willReturn($firstCall, $secondCall); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $configs = $this->client->configurations(); @@ -125,7 +125,7 @@ public function testCreateInstance() ->shouldBeCalled() ->willReturn([]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $config = $this->prophesize(Configuration::class); $config->name()->willReturn('my-config'); @@ -160,7 +160,7 @@ public function testInstances() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $instances = $this->client->instances(); $this->assertInstanceOf(\Generator::class, $instances); From 9b1c5e1ced92d2c5ffa83557a1140f2a7d8796b1 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 11 Jan 2017 10:51:30 -0500 Subject: [PATCH 034/107] Fix spanner snippets --- .../snippets/SpannerAdmin/ConfigurationTest.php | 6 +++--- tests/snippets/SpannerAdmin/DatabaseTest.php | 10 +++++----- tests/snippets/SpannerAdmin/InstanceTest.php | 16 ++++++++-------- .../snippets/SpannerAdmin/SpannerClientTest.php | 8 ++++---- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/tests/snippets/SpannerAdmin/ConfigurationTest.php b/tests/snippets/SpannerAdmin/ConfigurationTest.php index 02ea3e29ac90..185a4b2b8e63 100644 --- a/tests/snippets/SpannerAdmin/ConfigurationTest.php +++ b/tests/snippets/SpannerAdmin/ConfigurationTest.php @@ -73,7 +73,7 @@ public function testInfo() 'displayName' => self::CONFIG ]); - $this->config->setConnection($this->connection->reveal()); + $this->config->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals(self::CONFIG, $res->output()); @@ -91,7 +91,7 @@ public function testExists() 'displayName' => self::CONFIG ]); - $this->config->setConnection($this->connection->reveal()); + $this->config->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Configuration exists!', $res->output()); @@ -111,7 +111,7 @@ public function testReload() ->shouldBeCalled() ->willReturn($info); - $this->config->setConnection($this->connection->reveal()); + $this->config->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('info'); $this->assertEquals($info, $res->returnVal()); diff --git a/tests/snippets/SpannerAdmin/DatabaseTest.php b/tests/snippets/SpannerAdmin/DatabaseTest.php index d0252adb1382..2aec4676511a 100644 --- a/tests/snippets/SpannerAdmin/DatabaseTest.php +++ b/tests/snippets/SpannerAdmin/DatabaseTest.php @@ -69,7 +69,7 @@ public function testExists() ->shouldBeCalled() ->willReturn(['statements' => []]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Database exists!', $res->output()); @@ -83,7 +83,7 @@ public function testUpdateDdl() $this->connection->updateDatabase(Argument::any()) ->shouldBeCalled(); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -96,7 +96,7 @@ public function testUpdateDdlBatch() $this->connection->updateDatabase(Argument::any()) ->shouldBeCalled(); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -109,7 +109,7 @@ public function testDrop() $this->connection->dropDatabase(Argument::any()) ->shouldBeCalled(); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -130,7 +130,7 @@ public function testDdl() 'statements' => $stmts ]); - $this->database->setConnection($this->connection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('statements'); $this->assertEquals($stmts, $res->returnVal()); diff --git a/tests/snippets/SpannerAdmin/InstanceTest.php b/tests/snippets/SpannerAdmin/InstanceTest.php index 46fee5820b07..1cfcf9ae6a24 100644 --- a/tests/snippets/SpannerAdmin/InstanceTest.php +++ b/tests/snippets/SpannerAdmin/InstanceTest.php @@ -74,7 +74,7 @@ public function testInfo() ->shouldBeCalled() ->willReturn(['nodeCount' => 1]); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('1', $res->output()); @@ -89,7 +89,7 @@ public function testExists() ->shouldBeCalled() ->willReturn(['foo' => 'bar']); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Instance exists!', $res->output()); @@ -104,7 +104,7 @@ public function testReload() ->shouldBeCalledTimes(1) ->willReturn(['nodeCount' => 1]); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('info'); $info = $this->instance->info(); @@ -121,7 +121,7 @@ public function testState() ->shouldBeCalledTimes(1) ->willReturn(['state' => Instance::STATE_READY]); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke(); $this->assertEquals('Instance is ready!', $res->output()); @@ -142,7 +142,7 @@ public function testUpdate() $this->connection->updateInstance(Argument::any()) ->shouldBeCalled(); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -154,7 +154,7 @@ public function testDelete() $this->connection->deleteInstance(Argument::any()) ->shouldBeCalled(); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $snippet->invoke(); } @@ -166,7 +166,7 @@ public function testCreateDatabase() $this->connection->createDatabase(Argument::any()) ->shouldBeCalled(); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('database'); $this->assertInstanceOf(Database::class, $res->returnVal()); @@ -196,7 +196,7 @@ public function databases() ] ]); - $this->instance->setConnection($this->connection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('databases'); diff --git a/tests/snippets/SpannerAdmin/SpannerClientTest.php b/tests/snippets/SpannerAdmin/SpannerClientTest.php index 410935587e75..14e1a569d488 100644 --- a/tests/snippets/SpannerAdmin/SpannerClientTest.php +++ b/tests/snippets/SpannerAdmin/SpannerClientTest.php @@ -39,7 +39,7 @@ public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); $this->client = \Google\Cloud\Dev\stub(SpannerClient::class); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testConfigurations() @@ -53,7 +53,7 @@ public function testConfigurations() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $snippet = $this->snippetFromMethod(SpannerClient::class, 'configurations'); $snippet->addLocal('spanner', $this->client); @@ -88,7 +88,7 @@ public function testCreateInstance() ->shouldBeCalled() ->willReturn([]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('instance'); $this->assertInstanceOf(Instance::class, $res->returnVal()); @@ -119,7 +119,7 @@ public function testInstances() ] ]); - $this->client->setConnection($this->connection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $res = $snippet->invoke('instances'); $this->assertInstanceOf(\Generator::class, $res->returnVal()); From c1952d58c158676b39cb11c5207b17cca9168a0a Mon Sep 17 00:00:00 2001 From: Michael Bausor Date: Wed, 11 Jan 2017 08:56:24 -0800 Subject: [PATCH 035/107] Update service endpoint --- src/Spanner/Admin/Database/V1/DatabaseAdminClient.php | 6 +++--- src/Spanner/Admin/Instance/V1/InstanceAdminClient.php | 6 +++--- src/Spanner/V1/SpannerClient.php | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php index 47ac7e8bf9c6..1cdee6646c22 100644 --- a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php @@ -97,7 +97,7 @@ class DatabaseAdminClient /** * The default address of the service. */ - const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + const SERVICE_ADDRESS = 'spanner.googleapis.com'; /** * The default port of the service. @@ -261,14 +261,14 @@ public function getOperationsClient() * Optional. Options for configuring the service API wrapper. * * @type string $serviceAddress The domain name of the API remote host. - * Default 'wrenchworks.googleapis.com'. + * Default 'spanner.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from * \Grpc\ChannelCredentials::createSsl() * @type array $scopes A string array of scopes to use when acquiring credentials. - * Default the scopes for the Google Cloud Spanner Admin Database API. + * Default the scopes for the Google Cloud Spanner Database Admin API. * @type array $retryingOverride * An associative array of string => RetryOptions, where the keys * are method names (e.g. 'createFoo'), that overrides default retrying diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php index a47467fdb9c7..df798a185421 100644 --- a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -115,7 +115,7 @@ class InstanceAdminClient /** * The default address of the service. */ - const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + const SERVICE_ADDRESS = 'spanner.googleapis.com'; /** * The default port of the service. @@ -307,14 +307,14 @@ public function getOperationsClient() * Optional. Options for configuring the service API wrapper. * * @type string $serviceAddress The domain name of the API remote host. - * Default 'wrenchworks.googleapis.com'. + * Default 'spanner.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from * \Grpc\ChannelCredentials::createSsl() * @type array $scopes A string array of scopes to use when acquiring credentials. - * Default the scopes for the Google Cloud Spanner Admin Instance API. + * Default the scopes for the Google Cloud Spanner Instance Admin API. * @type array $retryingOverride * An associative array of string => RetryOptions, where the keys * are method names (e.g. 'createFoo'), that overrides default retrying diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index f5ce68ac1e16..6f26b186090b 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -84,7 +84,7 @@ class SpannerClient /** * The default address of the service. */ - const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + const SERVICE_ADDRESS = 'spanner.googleapis.com'; /** * The default port of the service. @@ -231,7 +231,7 @@ private static function getSessionNameTemplate() * Optional. Options for configuring the service API wrapper. * * @type string $serviceAddress The domain name of the API remote host. - * Default 'wrenchworks.googleapis.com'. + * Default 'spanner.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. From 7fa54e58f19c6c1adacd5c6af4abcbd1bdbf71d5 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 16 Jan 2017 10:03:31 -0500 Subject: [PATCH 036/107] WIP --- composer.json | 2 +- src/GrpcRequestWrapper.php | 5 ++ .../LongRunningConnectionInterface.php | 36 +++++++++ src/LongRunning/Operation.php | 75 +++++++++++++++++++ .../Connection/ConnectionInterface.php | 15 ++++ src/Spanner/Connection/Grpc.php | 38 ++++++++++ .../Connection/LongRunningConnection.php | 54 +++++++++++++ src/Spanner/Database.php | 5 +- 8 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 src/LongRunning/LongRunningConnectionInterface.php create mode 100644 src/LongRunning/Operation.php create mode 100644 src/Spanner/Connection/LongRunningConnection.php diff --git a/composer.json b/composer.json index 5d3a4305e8c9..bc4314da40cf 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", "google/proto-client-php": "dev-master", - "google/gax": "^0.5" + "google/gax": "^0.6" }, "suggest": { "google/gax": "Required to support gRPC", diff --git a/src/GrpcRequestWrapper.php b/src/GrpcRequestWrapper.php index 4d73e671f023..e8a33cc0937a 100644 --- a/src/GrpcRequestWrapper.php +++ b/src/GrpcRequestWrapper.php @@ -25,6 +25,7 @@ use Google\Cloud\PhpArray; use Google\Cloud\RequestWrapperTrait; use Google\GAX\ApiException; +use Google\GAX\OperationResponse; use Google\GAX\PagedListResponse; use Google\GAX\RetrySettings; use Grpc; @@ -148,6 +149,10 @@ private function handleResponse($response) return $response->serialize($this->codec); } + if ($response instanceof OperationResponse) { + return $response; + } + return null; } diff --git a/src/LongRunning/LongRunningConnectionInterface.php b/src/LongRunning/LongRunningConnectionInterface.php new file mode 100644 index 000000000000..3a871a3a107e --- /dev/null +++ b/src/LongRunning/LongRunningConnectionInterface.php @@ -0,0 +1,36 @@ +connection = $connection; + $this->name = $name; + $this->type = $type; + $this->info = $info; + } + + public function name() + { + return $this->name; + } + + public function reload(array $options = []) + { + $this->info = $info = $this->connection->getOperation([ + 'name' => $this->name, + 'type' => $this->type + ] + $options); + + return $info; + } + + public function info(array $options = []) + { + return $this->info ?: $this->reload($options); + } + + public function wait(array $options = []) + { + do { + $this->reload($options); + } while(true); + } + + public function cancel(array $options = []) + { + return $this->connection->cancelOperation([ + 'name' => $this->name + ] + $options); + } + + public function delete(array $options = []) + { + return $this->connection->deleteOperation([ + 'name' => $this->name + ] + $options); + } +} diff --git a/src/Spanner/Connection/ConnectionInterface.php b/src/Spanner/Connection/ConnectionInterface.php index e041a94c9f10..99d005a3b7d6 100644 --- a/src/Spanner/Connection/ConnectionInterface.php +++ b/src/Spanner/Connection/ConnectionInterface.php @@ -148,4 +148,19 @@ public function commit(array $args = []); * @param array $args [optional] */ public function rollback(array $args = []); + + /** + * @param array $args + */ + public function getOperation(array $args); + + /** + * @param array $args + */ + public function cancelOperation(array $args); + + /** + * @param array $args + */ + public function deleteOperation(array $args); } diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 280c6a071783..ae458e1352a8 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -38,6 +38,14 @@ class Grpc implements ConnectionInterface { use GrpcTrait; + const DATABASE_LRO_TYPE = ''; + const OPERATION_LRO_TYPE = ''; + + private $lroTypes = [ + self::DATABASE_LRO_TYPE, + self::OPERATION_LRO_TYPE + ]; + /** * @var InstanceAdminClient */ @@ -53,6 +61,11 @@ class Grpc implements ConnectionInterface */ private $spannerClient; + /** + * @var OperationsClient + */ + private $operationsClient; + /** * @var CodecInterface */ @@ -92,6 +105,7 @@ public function __construct(array $config = []) $this->instanceAdminClient = new InstanceAdminClient($grpcConfig); $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); $this->spannerClient = new SpannerClient($grpcConfig); + $this->operationsClient = $this->instanceAdminClient->getOperationsClient(); } /** @@ -487,6 +501,30 @@ public function rollback(array $args = []) ]); } + /** + * @param array $args + */ + public function getOperation(array $args) + { + + } + + /** + * @param array $args + */ + public function cancelOperation(array $args) + { + + } + + /** + * @param array $args + */ + public function deleteOperation(array $args) + { + + } + /** * @param array $keySet * @return array Formatted keyset diff --git a/src/Spanner/Connection/LongRunningConnection.php b/src/Spanner/Connection/LongRunningConnection.php new file mode 100644 index 000000000000..43e18584d8f2 --- /dev/null +++ b/src/Spanner/Connection/LongRunningConnection.php @@ -0,0 +1,54 @@ +connection = $connection; + } + + /** + * @param array $args + */ + public function getOperation(array $args) + { + return $this->connection->getOperation($args); + } + + /** + * @param array $args + */ + public function cancelOperation(array $args) + { + return $this->connection->cancelOperation($args); + } + + /** + * @param array $args + */ + public function deleteOperation(array $args) + { + return $this->connection->deleteOperation($args); + } +} diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 3d531b381c51..6ac777ba5507 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -20,6 +20,7 @@ use Google\Cloud\ArrayTrait; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; +use Google\Cloud\LongRunning\LROTrait; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Google\Cloud\Spanner\Session\SessionPoolInterface; @@ -225,10 +226,12 @@ public function updateDdlBatch(array $statements, array $options = []) 'operationId' => null ]; - return $this->connection->updateDatabase($options + [ + $res = $this->connection->updateDatabase($options + [ 'name' => $this->fullyQualifiedDatabaseName(), 'statements' => $statements, ]); + + return $this->longRunningResponse($res); } /** From 4e220b1504676935222b17018955489bef473c9e Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 16 Jan 2017 11:06:24 -0500 Subject: [PATCH 037/107] Updated Documentation --- src/Spanner/Bytes.php | 23 +++++- src/Spanner/Configuration.php | 15 +++- src/Spanner/Database.php | 74 ++++++++++-------- src/Spanner/Date.php | 23 +++++- src/Spanner/Instance.php | 4 +- src/Spanner/KeyRange.php | 25 ++++++ src/Spanner/KeySet.php | 54 +++++++++++++ src/Spanner/Result.php | 54 +++++++++++-- src/Spanner/SpannerClient.php | 16 +++- src/Spanner/Timestamp.php | 2 +- src/Spanner/Transaction.php | 142 +++++++++++++++++++++++++++++++++- 11 files changed, 378 insertions(+), 54 deletions(-) diff --git a/src/Spanner/Bytes.php b/src/Spanner/Bytes.php index 7a0710582549..f0e3e4d6daf3 100644 --- a/src/Spanner/Bytes.php +++ b/src/Spanner/Bytes.php @@ -22,7 +22,7 @@ /** * Represents a value with a data type of - * [bytes](https://cloud.google.com/spanner/reference/rest/v1/ResultSetMetadata#typecode). + * [bytes](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TypeCode). * * Example: * ``` @@ -33,6 +33,11 @@ * * $bytes = $spanner->bytes('hello world'); * ``` + * + * ``` + * // Bytes objects can be cast to strings for easy display. + * echo (string) $bytes; + * ``` */ class Bytes implements ValueInterface { @@ -52,6 +57,11 @@ public function __construct($value) /** * Get the bytes as a stream. * + * Example: + * ``` + * $stream = $bytes->get(); + * ``` + * * @return StreamInterface */ public function get() @@ -62,6 +72,11 @@ public function get() /** * Get the type. * + * Example: + * ``` + * echo $bytes->type(); + * ``` + * * @return string */ public function type() @@ -72,6 +87,11 @@ public function type() /** * Format the value as a string. * + * Example: + * ``` + * echo $bytes->formatAsString(); + * ``` + * * @return string */ public function formatAsString() @@ -83,6 +103,7 @@ public function formatAsString() * Format the value as a string. * * @return string + * @access private */ public function __toString() { diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php index 4a51b5575417..983b1204a2c2 100644 --- a/src/Spanner/Configuration.php +++ b/src/Spanner/Configuration.php @@ -34,7 +34,9 @@ * $configuration = $spanner->configuration('regional-europe-west'); * ``` * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs Instance Configs + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig InstanceConfig + * @codingStandardsIgnoreEnd */ class Configuration { @@ -100,15 +102,16 @@ public function name() * * This method may require a service call. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $info = $configuration->info(); - * echo $info['displayName']; * ``` * * @codingStandardsIgnoreStart * @param array $options [optional] Configuration options. - * @return array [InstanceConfig](https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs#InstanceConfig) + * @return array [InstanceConfig](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig) * @codingStandardsIgnoreEnd */ public function info(array $options = []) @@ -125,6 +128,8 @@ public function info(array $options = []) * * This method requires a service call. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * if ($configuration->exists()) { @@ -149,6 +154,8 @@ public function exists(array $options = []) /** * Fetch a fresh representation of the configuration from the service. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $info = $configuration->reload(); @@ -156,7 +163,7 @@ public function exists(array $options = []) * * @codingStandardsIgnoreStart * @param array $options [optional] Configuration options. - * @return array [InstanceConfig](https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs#InstanceConfig) + * @return array [InstanceConfig](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig) * @codingStandardsIgnoreEnd */ public function reload(array $options = []) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 3d531b381c51..2a5b1ea1f847 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -142,6 +142,8 @@ public function name() * * This method sends a service request. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * if ($database->exists()) { @@ -166,6 +168,8 @@ public function exists(array $options = []) /** * Update the Database schema by running a SQL statement. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $database->updateDdl( @@ -194,6 +198,8 @@ public function updateDdl($statement, array $options = []) /** * Update the Database schema by running a set of SQL statements. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $database->updateDdlBatch([ @@ -234,6 +240,8 @@ public function updateDdlBatch(array $statements, array $options = []) /** * Drop the database. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $database->drop(); @@ -256,6 +264,8 @@ public function drop(array $options = []) /** * Get a list of all database DDL statements. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $statements = $database->ddl(); @@ -302,18 +312,22 @@ public function iam() * If no configuration options are provided, transaction will be opened with * strong consistency. * - * @codingStandardsIgnoreStart - * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest - * @codingStandardsIgnoreEnd + * Example: + * ``` + * $transaction = $database->readOnlyTransaction(); + * ``` * * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest * @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`, `$minReadTimestamp`, `$maxStaleness`, - * `$readTimestamp` or `$exactStaleness` may be set in a request. + * for detailed description of available options. + * + * Please note that only one of `$strong`, `$minReadTimestamp`, + * `$maxStaleness`, `$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 @@ -386,6 +400,11 @@ public function readOnlyTransaction(array $options = []) /** * Create a Read/Write transaction * + * Example: + * ``` + * $transaction = $database->readWriteTransaction(); + * ``` + * * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest * @codingStandardsIgnoreEnd @@ -393,7 +412,7 @@ public function readOnlyTransaction(array $options = []) * @param array $options [optional] Configuration Options * @return Transaction */ - public function lockingTransaction(array $options = []) + public function readWriteTransaction(array $options = []) { $options['transactionOptions'] = [ 'readWrite' => [] @@ -759,18 +778,31 @@ public function execute($sql, array $options = []) * Note that if no KeySet is specified, all rows in a table will be * returned. * + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [1337] + * ]); + * + * $result = $database->read('Posts', [ + * 'keySet' => $keySet + * ]); + * ``` + * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest ReadRequest * + * @codingStandardsIgnoreStart * @param string $table The table name. * @param array $options [optional] { * Configuration Options. * * @type string $index The name of an index on the table. * @type array $columns A list of column names to be returned. - * @type KeySet $keySet A [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). + * @type KeySet $keySet A [KeySet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#keyset). * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. * } + * @codingStandardsIgnoreEnd */ public function read($table, array $options = []) { @@ -779,32 +811,6 @@ public function read($table, array $options = []) return $this->operation->read($session, $table, $options); } - /** - * Create a transaction with a given context. - * - * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest - * - * @param string $context The context of the new transaction. - * @param array $options [optional] Configuration options. - * @return Transaction - */ - private function transaction($context, array $options = []) - { - $options += [ - 'transactionOptions' => [] - ]; - - $session = $this->selectSession($context); - - // make a service call here. - $res = $this->connection->beginTransaction($options + [ - 'session' => $session->name(), - 'context' => $context, - ]); - - return new Transaction($this->operation, $session, $context, $res); - } - /** * Retrieve a session from the session pool. * diff --git a/src/Spanner/Date.php b/src/Spanner/Date.php index 9d74cb2fd954..777f18b48840 100644 --- a/src/Spanner/Date.php +++ b/src/Spanner/Date.php @@ -19,7 +19,7 @@ /** * Represents a value with a data type of - * [Date](https://cloud.google.com/spanner/reference/rest/v1/ResultSetMetadata#typecode). + * [Date](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TypeCode). * * Example: * ``` @@ -30,6 +30,11 @@ * * $date = $spanner->date(new \DateTime('1995-02-04')); * ``` + * + * ``` + * // Date objects can be cast to strings for easy display. + * echo (string) $date; + * ``` */ class Date implements ValueInterface { @@ -51,6 +56,11 @@ public function __construct(\DateTimeInterface $value) /** * Get the underlying `\DateTimeInterface` implementation. * + * Example: + * ``` + * $dateTime = $date->get(); + * ``` + * * @return \DateTimeInterface */ public function get() @@ -61,6 +71,11 @@ public function get() /** * Get the type. * + * Example: + * ``` + * echo $date->type(); + * ``` + * * @return string */ public function type() @@ -71,6 +86,11 @@ public function type() /** * Format the value as a string. * + * Example: + * ``` + * echo $date->formatAsString(); + * ``` + * * @return string */ public function formatAsString() @@ -82,6 +102,7 @@ public function formatAsString() * Format the value as a string. * * @return string + * @access private */ public function __toString() { diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index af8fc8c97c31..622d982c4ea0 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -361,7 +361,9 @@ public function database($name) * $databases = $instance->databases(); * ``` * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/list List Databases + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.database.v1#listdatabasesrequest ListDatabasesRequest + * @codingStandardsIgnoreEnd * * @param array $options Configuration options. * @return \Generator diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php index 37b9f090c7b0..4407745f00ac 100644 --- a/src/Spanner/KeyRange.php +++ b/src/Spanner/KeyRange.php @@ -107,6 +107,11 @@ public function __construct(array $options = []) /** * Get the range start. * + * Example: + * ``` + * $start = $range->start(); + * ``` + * * @return array */ public function start() @@ -118,6 +123,11 @@ public function start() /** * Set the range start. * + * Example: + * ``` + * $range->setStart(KeyRange::TYPE_OPEN, ['Bob']); + * ``` + * * @param string $type Either "open" or "closed". Use constants * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed * correctness. @@ -142,6 +152,11 @@ public function setStart($type, array $start) /** * Get the range end. * + * Example: + * ``` + * $end = $range->end(); + * ``` + * * @return array */ public function end() @@ -153,6 +168,11 @@ public function end() /** * Set the range end. * + * Example: + * ``` + * $range->setEnd(KeyRange::TYPE_CLOSED, ['Jill']); + * ``` + * * @param string $type Either "open" or "closed". Use constants * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed * correctness. @@ -177,6 +197,11 @@ public function setEnd($type, array $end) /** * Get the start and end types * + * Example: + * ``` + * $types = $range->types(); + * ``` + * * @return array */ public function types() diff --git a/src/Spanner/KeySet.php b/src/Spanner/KeySet.php index b8390f72de09..7aa199291624 100644 --- a/src/Spanner/KeySet.php +++ b/src/Spanner/KeySet.php @@ -22,6 +22,16 @@ /** * Represents a Google Cloud Spanner KeySet. * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $keySet = $spanner->keySet(); + * ``` + * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#keyset KeySet */ class KeySet @@ -73,6 +83,11 @@ public function __construct(array $options = []) /** * Fetch the KeyRanges * + * Example: + * ``` + * $ranges = $keySet->ranges(); + * ``` + * * @return KeyRange[] */ public function ranges() @@ -84,6 +99,12 @@ public function ranges() /** * Add a single KeyRange. * + * Example: + * ``` + * $range = $spanner->keyRange(); + * $keySet->addRange($range); + * ``` + * * @param KeyRange $range A KeyRange instance. * @return void */ @@ -97,6 +118,12 @@ public function addRange(KeyRange $range) * * Any existing KeyRanges will be overridden. * + * Example: + * ``` + * $range = $spanner->keyRange(); + * $keySet->setRanges([$range]); + * ``` + * * @param KeyRange[] $ranges An array of KeyRange objects. * @return void */ @@ -110,6 +137,11 @@ public function setRanges(array $ranges) /** * Fetch the keys. * + * Example: + * ``` + * $keys = $keySet->keys(); + * ``` + * * @return mixed[] */ public function keys() @@ -123,6 +155,11 @@ public function keys() * A Key should have exactly as many elements as there are columns in the * primary or index key with which this KeySet is used. * + * Example: + * ``` + * $keySet->addKey('Bob'); + * ``` + * * @param mixed $key The Key to add. * @return void */ @@ -136,6 +173,11 @@ public function addKey($key) * * Any existing keys will be overridden. * + * Example: + * ``` + * $keySet->setKeys(['Bob', 'Jill']); + * ``` + * * @param mixed[] $keys * @return void */ @@ -147,6 +189,13 @@ public function setKeys(array $keys) /** * Get the value of Match All. * + * Example: + * ``` + * if ($keySet->matchAll()) { + * echo "All keys will match"; + * } + * ``` + * * @return bool */ public function matchAll() @@ -157,6 +206,11 @@ public function matchAll() /** * Choose whether the KeySet should match all keys in a table. * + * Example: + * ``` + * $keySet->matchAll(true); + * ``` + * * @param bool $all If true, all keys in a table will be matched. * @return void */ diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index ca32428c77dc..121397d433f1 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -18,7 +18,20 @@ namespace Google\Cloud\Spanner; /** - * @todo should this be more like BigQuery\QueryResults? + * Represent a Google Cloud Spanner lookup result (either read or executeSql). + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * $database = $spanner->connect('my-instance', 'my-database'); + * + * $result = $database->execute('SELECT * FROM Posts'); + * ``` + * + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet ResultSet */ class Result implements \IteratorAggregate { @@ -45,7 +58,14 @@ public function __construct(array $result, array $rows) /** * Return result metadata * - * @return array [ResultSetMetadata](https://cloud.google.com/spanner/reference/rest/v1/ResultSetMetadata). + * Example: + * ``` + * $metadata = $result->metadata(); + * ``` + * + * @codingStandardsIgnoreStart + * @return array [ResultSetMetadata](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetMetadata). + * @codingStandardsIgnoreEnd */ public function metadata() { @@ -55,6 +75,11 @@ public function metadata() /** * Return the formatted and decoded rows. * + * Example: + * ``` + * $rows = $result->rows(); + * ``` + * * @return array|null */ public function rows() @@ -68,9 +93,21 @@ public function rows() * * Stats are not returned by default. * - * @todo explain how to get dem stats. + * Example: + * ``` + * $stats = $result->stats(); + * ``` * - * @return array|null [ResultSetStats](https://cloud.google.com/spanner/reference/rest/v1/ResultSetStats). + * ``` + * // Executing a query with stats returned. + * $res = $database->execute('SELECT * FROM Posts', [ + * 'queryMode' => 'PROFILE' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @return array|null [ResultSetStats](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetStats). + * @codingStandardsIgnoreEnd */ public function stats() { @@ -82,7 +119,14 @@ public function stats() /** * Get the entire query or read response as given by the API. * - * @return array [ResultSet](https://cloud.google.com/spanner/reference/rest/v1/ResultSet). + * Example: + * ``` + * $info = $result->info(); + * ``` + * + * @codingStandardsIgnoreStart + * @return array [ResultSet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet). + * @codingStandardsIgnoreEnd */ public function info() { diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index e6647592a0e7..55c169534676 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -130,7 +130,9 @@ public function __construct(array $config = []) * $configurations = $spanner->configurations(); * ``` * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs/list List Configs + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.ListInstanceConfigsRequest ListInstanceConfigsRequest + * @codingStandardsIgnoreEnd * * @param array $options [optional] Configuration Options. * @return Generator @@ -171,6 +173,10 @@ public function configurations(array $options = []) * $configuration = $spanner->configuration($configurationName); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#getinstanceconfigrequest GetInstanceConfigRequest + * @codingStandardsIgnoreEnd + * * @param string $name The Configuration name. * @param array $config [optional] The configuration details. * @return Configuration @@ -188,9 +194,9 @@ public function configuration($name, array $config = []) * $instance = $spanner->createInstance($configuration, 'my-instance'); * ``` * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/create Create Instance - * * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#createinstancerequest CreateInstanceRequest + * * @param Configuration $config The configuration to use * @param string $name The instance name * @param array $options [optional] { @@ -258,7 +264,9 @@ public function instance($name, array $instance = []) * * @todo implement pagination! * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/list List Instances + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#listinstancesrequest ListInstancesRequest + * @codingStandardsIgnoreEnd * * @param array $options [optional] Configuration options * @return Generator diff --git a/src/Spanner/Timestamp.php b/src/Spanner/Timestamp.php index de27388f272d..8968315d9d0e 100644 --- a/src/Spanner/Timestamp.php +++ b/src/Spanner/Timestamp.php @@ -19,7 +19,7 @@ /** * Represents a value with a data type of - * [Timestamp](https://cloud.google.com/spanner/reference/rest/v1/ResultSetMetadata#typecode). + * [Timestamp](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TypeCode). * * Nanosecond precision is preserved by passing nanoseconds as a separate * argument to the constructor. If nanoseconds are given, any subsecond diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 8ed22cdef6cb..d10454ce762c 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -22,7 +22,18 @@ use RuntimeException; /** - * Enabled interaction with Google Cloud Spanner inside a Transaction. + * Manages interaction with Google Cloud Spanner inside a Transaction. + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $database = $spanner->connect('my-instance', 'my-database'); + * $transaction = $database->readWriteTransaction(); + * ``` */ class Transaction { @@ -80,6 +91,15 @@ public function __construct( /** * Enqueue an insert mutation. * + * Example: + * ``` + * $transaction->insert('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ]); + * ``` + * * @param string $table The table to insert into. * @param array $data The data to insert. * @return Transaction The transaction, to enable method chaining. @@ -92,6 +112,17 @@ public function insert($table, array $data) /** * Enqueue one or more insert mutations. * + * Example: + * ``` + * $transaction->insertBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ] + * ]); + * ``` + * * @param string $table The table to insert into. * @param array $dataSet The data to insert. * @return Transaction The transaction, to enable method chaining. @@ -106,6 +137,15 @@ public function insertBatch($table, array $dataSet) /** * Enqueue an update mutation. * + * Example: + * ``` + * $transaction->update('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post [Updated!]', + * 'content' => 'Modified Content' + * ]); + * ``` + * * @param string $table The table to update. * @param array $data The data to update. * @return Transaction The transaction, to enable method chaining. @@ -118,6 +158,17 @@ public function update($table, array $data) /** * Enqueue one or more update mutations. * + * Example: + * ``` + * $transaction->updateBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post [Updated!]', + * 'content' => 'Modified Content' + * ] + * ]); + * ``` + * * @param string $table The table to update. * @param array $dataSet The data to update. * @return Transaction The transaction, to enable method chaining. @@ -132,6 +183,15 @@ public function updateBatch($table, array $dataSet) /** * Enqueue an insert or update mutation. * + * Example: + * ``` + * $transaction->insertOrUpdate('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ]); + * ``` + * * @param string $table The table to insert into or update. * @param array $data The data to insert or update. * @return Transaction The transaction, to enable method chaining. @@ -144,6 +204,17 @@ public function insertOrUpdate($table, array $data) /** * Enqueue one or more insert or update mutations. * + * Example: + * ``` + * $transaction->insertOrUpdateBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ] + * ]); + * ``` + * * @param string $table The table to insert into or update. * @param array $dataSet The data to insert or update. * @return Transaction The transaction, to enable method chaining. @@ -158,6 +229,15 @@ public function insertOrUpdateBatch($table, array $dataSet) /** * Enqueue an replace mutation. * + * Example: + * ``` + * $transaction->replace('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post [Replaced]', + * 'content' => 'Hello Moon' + * ]); + * ``` + * * @param string $table The table to replace into. * @param array $data The data to replace. * @return Transaction The transaction, to enable method chaining. @@ -170,6 +250,17 @@ public function replace($table, array $data) /** * Enqueue one or more replace mutations. * + * Example: + * ``` + * $transaction->replaceBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post [Replaced]', + * 'content' => 'Hello Moon' + * ] + * ]); + * ``` + * * @param string $table The table to replace into. * @param array $dataSet The data to replace. * @return Transaction The transaction, to enable method chaining. @@ -184,6 +275,15 @@ public function replaceBatch($table, array $dataSet) /** * Enqueue an delete mutation. * + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [10] + * ]); + * + * $transaction->delete('Posts', $keySet); + * ``` + * * @param string $table The table to mutate. * @param KeySet $keySet The KeySet to identify rows to delete. * @return Transaction The transaction, to enable method chaining. @@ -232,18 +332,29 @@ public function execute($sql, array $options = []) * Note that if no KeySet is specified, all rows in a table will be * returned. * - * @todo is returning everything a reasonable default? + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [10] + * ]); * + * $result = $database->read('Posts', [ + * 'keySet' => $keySet + * ]); + * ``` + * + * @codingStandardsIgnoreStart * @param string $table The table name. * @param array $options [optional] { * Configuration Options. * * @type string $index The name of an index on the table. * @type array $columns A list of column names to be returned. - * @type array $keySet A [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). + * @type array $keySet A [KeySet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.KeySet). * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. * } + * @codingStandardsIgnoreEnd */ public function read($table, array $options = []) { @@ -257,6 +368,11 @@ public function read($table, array $options = []) * * This closes the transaction, preventing any future API calls inside it. * + * Example: + * ``` + * $transaction->commit(); + * ``` + * * @param array $options [optional] Configuration Options. * @return Timestamp The commit Timestamp. */ @@ -282,6 +398,11 @@ public function commit(array $options = []) * * Rollback will NOT error if the transaction is not found or was already aborted. * + * Example: + * ``` + * $transaction->rollback(); + * ``` + * * @param array $options [optional] Configuration Options. * @return void */ @@ -296,6 +417,11 @@ public function rollback(array $options = []) * For snapshot read-only transactions, the read timestamp chosen for the * transaction. * + * Example: + * ``` + * $timestamp = $transaction->readTimestamp(); + * ``` + * * @return Timestamp */ public function readTimestamp() @@ -306,6 +432,11 @@ public function readTimestamp() /** * Retrieve the Transaction ID. * + * Example: + * ``` + * $id = $transaction->id(); + * ``` + * * @return string */ public function id() @@ -316,6 +447,11 @@ public function id() /** * Retrieve the Transaction Context * + * Example: + * ``` + * $context = $transaction->context(); + * ``` + * * @return string */ public function context() From f4fce6b97f9ed45309e7ead82dfff273c9cab7ad Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 16 Jan 2017 13:08:20 -0500 Subject: [PATCH 038/107] Update KeySet reference doc --- src/Spanner/Database.php | 2 +- src/Spanner/Transaction.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 2a5b1ea1f847..0c450db284b5 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -798,7 +798,7 @@ public function execute($sql, array $options = []) * * @type string $index The name of an index on the table. * @type array $columns A list of column names to be returned. - * @type KeySet $keySet A [KeySet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#keyset). + * @type KeySet $keySet A KeySet defining which rows to return. * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. * } diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index d10454ce762c..630e8904ab78 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -350,7 +350,7 @@ public function execute($sql, array $options = []) * * @type string $index The name of an index on the table. * @type array $columns A list of column names to be returned. - * @type array $keySet A [KeySet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.KeySet). + * @type KeySet $keySet A KeySet, defining which rows to return. * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. * } From bb53c4e065ab14bb3738db9bb8fff6b67c204d95 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 16 Jan 2017 15:57:01 -0500 Subject: [PATCH 039/107] Run transactions inside a callable --- src/Spanner/Database.php | 22 ++++++++++++++++------ src/Spanner/Operation.php | 20 +++++++++++++++----- src/Spanner/Transaction.php | 36 +++++++++++------------------------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 0c450db284b5..6ce1cfa16b0e 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -307,7 +307,7 @@ public function iam() } /** - * Create a Read Only transaction. + * Execute read operations inside a transaction. * * If no configuration options are provided, transaction will be opened with * strong consistency. @@ -319,6 +319,8 @@ public function iam() * * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @param callable $operation The operations to run in the transaction. + * **Signature:** `function (Transaction $transaction)`. * @param array $options [optional] { * Configuration Options * @@ -347,7 +349,7 @@ public function iam() * @codingStandardsIgnoreEnd * @return Transaction */ - public function readOnlyTransaction(array $options = []) + public function readOnlyTransaction(callable $operation, array $options = []) { $options += [ 'returnReadTimestamp' => null, @@ -394,11 +396,13 @@ public function readOnlyTransaction(array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); - return $this->operation->transaction($session, SessionPoolInterface::CONTEXT_READ, $options); + $transaction = $this->operation->transaction($session, SessionPoolInterface::CONTEXT_READ, $options); + + return call_user_func($operation, $transaction); } /** - * Create a Read/Write transaction + * Execute Read/Write operations inside a Transaction. * * Example: * ``` @@ -409,10 +413,12 @@ public function readOnlyTransaction(array $options = []) * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest * @codingStandardsIgnoreEnd * + * @param callable $operation The operations to run in the transaction. + * **Signature:** `function (Transaction $transaction)`. * @param array $options [optional] Configuration Options * @return Transaction */ - public function readWriteTransaction(array $options = []) + public function readWriteTransaction(callable $operation, array $options = []) { $options['transactionOptions'] = [ 'readWrite' => [] @@ -420,7 +426,11 @@ public function readWriteTransaction(array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - return $this->operation->transaction($session, SessionPoolInterface::CONTEXT_READWRITE, $options); + $transaction = $this->operation->transaction($session, SessionPoolInterface::CONTEXT_READWRITE, $options); + + call_user_func($operation, $transaction); + + return $this->operation->commit($session, $transaction, $options); } /** diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 931986188acb..d1a8b0e16d10 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -20,6 +20,7 @@ use Google\Cloud\ArrayTrait; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Session\Session; +use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\ValidateTrait; /** @@ -110,18 +111,23 @@ public function deleteMutation($table, KeySet $keySet) * * @codingStandardsIgnoreStart * @param Session $session The session ID to use for the commit. - * @param array $mutations The mutations to commit. + * @param Transaction $transaction The transaction to commit. * @param array $options [optional] Configuration options. * @return Timestamp The commit Timestamp. */ - public function commit(Session $session, array $mutations, array $options = []) + public function commit(Session $session, Transaction $transaction, array $options = []) { + if ($transaction->context() !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new \RuntimeException('Cannot commit in a Read-Only Transaction'); + } + if (!isset($options['transactionId'])) { $options['singleUseTransaction'] = ['readWrite' => []]; } $res = $this->connection->commit([ - 'mutations' => $mutations, + 'transactionId' => $transaction->id(), + 'mutations' => $transaction->mutations(), 'session' => $session->name() ] + $options); @@ -134,12 +140,16 @@ public function commit(Session $session, array $mutations, array $options = []) * @param Session $session The session to use for the rollback. * Note that the session MUST be the same one in which the * transaction was created. - * @param string $transactionId The transaction to roll back. + * @param Transaction $transaction The transaction to roll back. * @param array $options [optional] Configuration Options. * @return void */ - public function rollback(Session $session, $transactionId, array $options = []) + public function rollback(Session $session, Transaction $transaction, array $options = []) { + if ($transaction->context() !== SessionPoolInterface::CONTEXT_READWRITE) { + throw new \RuntimeException('Cannot rollback a Read-Only Transaction'); + } + return $this->connection->rollback([ 'transactionId' => $transactionId, 'session' => $session->name() diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 630e8904ab78..0911e43cb460 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -363,30 +363,6 @@ public function read($table, array $options = []) ] + $options); } - /** - * Commit all mutations in a transaction. - * - * This closes the transaction, preventing any future API calls inside it. - * - * Example: - * ``` - * $transaction->commit(); - * ``` - * - * @param array $options [optional] Configuration Options. - * @return Timestamp The commit Timestamp. - */ - public function commit(array $options = []) - { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException('Cannot commit in a Read-Only Transaction'); - } - - return $this->operation->commit($this->session, $this->mutations, [ - 'transactionId' => $this->transactionId - ] + $options); - } - /** * Roll back a transaction. * @@ -408,7 +384,7 @@ public function commit(array $options = []) */ public function rollback(array $options = []) { - return $this->operation->rollback($this->session, $this->transactionId, $options); + return $this->operation->rollback($this->session, $this, $options); } /** @@ -459,6 +435,16 @@ public function context() return $this->context; } + /** + * Retrieve a list of formatted mutations. + * + * @return array + */ + public function mutations() + { + return $this->mutations; + } + /** * Format, validate and enqueue mutations in the transaction. * From 37e90dbd1b56d5d9101c9891f3e21fedd364036b Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Tue, 17 Jan 2017 14:54:30 -0500 Subject: [PATCH 040/107] blah --- src/Spanner/Database.php | 2 +- src/Spanner/Operation.php | 8 +++++ src/Spanner/Session/SessionClient.php | 19 +++++++----- src/Spanner/Transaction.php | 44 +++++++++++++++++++++++++++ src/Spanner/V1/SpannerClient.php | 2 +- 5 files changed, 66 insertions(+), 9 deletions(-) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 6ce1cfa16b0e..750635b15192 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -430,7 +430,7 @@ public function readWriteTransaction(callable $operation, array $options = []) call_user_func($operation, $transaction); - return $this->operation->commit($session, $transaction, $options); + // return $this->operation->commit($session, $transaction, $options); } /** diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index d1a8b0e16d10..bad28687b1ba 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -125,12 +125,20 @@ public function commit(Session $session, Transaction $transaction, array $option $options['singleUseTransaction'] = ['readWrite' => []]; } + echo 'Committing in Session '. $session->name() . PHP_EOL; + echo 'Calling commit ' . $transaction->id() . PHP_EOL . PHP_EOL; + + echo microtime(true);echo PHP_EOL.PHP_EOL; + $res = $this->connection->commit([ 'transactionId' => $transaction->id(), 'mutations' => $transaction->mutations(), 'session' => $session->name() ] + $options); + echo 'Commit done ' . $transaction->id() . PHP_EOL . PHP_EOL; + echo microtime(true);echo PHP_EOL.PHP_EOL; + return $this->mapper->createTimestampWithNanos($res['commitTimestamp']); } diff --git a/src/Spanner/Session/SessionClient.php b/src/Spanner/Session/SessionClient.php index 3cb21ff3b22d..7024f198a5f8 100644 --- a/src/Spanner/Session/SessionClient.php +++ b/src/Spanner/Session/SessionClient.php @@ -82,18 +82,23 @@ public function create($instance, $database, array $options = []) $session = null; if (isset($res['name'])) { - $session = new Session( - $this->connection, - $this->projectId, - SpannerClient::parseInstanceFromSessionName($res['name']), - SpannerClient::parseDatabaseFromSessionName($res['name']), - SpannerClient::parseSessionFromSessionName($res['name']) - ); + $session = $this->session($res['name']); } return $session; } + public function session($sessionName) + { + return new Session( + $this->connection, + $this->projectId, + SpannerClient::parseInstanceFromSessionName($sessionName), + SpannerClient::parseDatabaseFromSessionName($sessionName), + SpannerClient::parseSessionFromSessionName($sessionName) + ); + } + public function __debugInfo() { return [ diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 0911e43cb460..7e6098fb45ef 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -37,6 +37,10 @@ */ class Transaction { + const STATE_ACTIVE = 0; + const STATE_ROLLED_BACK = 1; + const STATE_COMMITTED = 2; + /** * @var Operation */ @@ -67,6 +71,11 @@ class Transaction */ private $mutations = []; + /** + * @var int + */ + private $state = self::STATE_ACTIVE; + /** * @param Operation $operation The Operation instance. * @param Session $session The session to use for spanner interactions. @@ -384,9 +393,26 @@ public function read($table, array $options = []) */ public function rollback(array $options = []) { + if ($this->state !== self::STATE_ACTIVE) { + throw new \RuntimeException('The transaction cannot be rolled back because it is not active'); + } + + $this->state = self::STATE_ROLLED_BACK; + return $this->operation->rollback($this->session, $this, $options); } + public function commit() + { + if ($this->state !== self::STATE_ACTIVE) { + throw new \RuntimeException('The transaction cannot be committed because it is not active'); + } + + $this->state = self::STATE_COMMITTED; + + return $this->operation->commit($this->session, $this); + } + /** * Retrieve the Read Timestamp. * @@ -435,6 +461,24 @@ public function context() return $this->context; } + /** + * Retrieve the Transaction State. + * + * Will be one of `Transaction::STATE_ACTIVE`, + * `Transaction::STATE_COMMITTED`, or `Transaction::STATE_ROLLED_BACK`. + * + * Example: + * ``` + * $state = $transaction->state(); + * ``` + * + * @return int + */ + public function state() + { + return $this->state; + } + /** * Retrieve a list of formatted mutations. * diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index 6f26b186090b..982c60fb0ef8 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -84,7 +84,7 @@ class SpannerClient /** * The default address of the service. */ - const SERVICE_ADDRESS = 'spanner.googleapis.com'; + const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; /** * The default port of the service. From 49ada7cf8208eff53f4740733227017bf86fb1e7 Mon Sep 17 00:00:00 2001 From: Michael Bausor Date: Wed, 18 Jan 2017 21:22:48 -0800 Subject: [PATCH 041/107] Regenerate spanner with streaming methods --- .../Admin/Database/V1/DatabaseAdminClient.php | 77 +++++- .../Admin/Instance/V1/InstanceAdminClient.php | 78 +++++- src/Spanner/V1/SpannerClient.php | 235 ++++++++++++++++++ 3 files changed, 382 insertions(+), 8 deletions(-) diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php index 1cdee6646c22..332d10cc7728 100644 --- a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php @@ -34,6 +34,7 @@ use Google\GAX\GrpcConstants; use Google\GAX\GrpcCredentialsHelper; use Google\GAX\LongRunning\OperationsClient; +use Google\GAX\OperationResponse; use Google\GAX\PageStreamingDescriptor; use Google\GAX\PathTemplate; use google\iam\v1\GetIamPolicyRequest; @@ -248,11 +249,42 @@ private static function getLongRunningDescriptors() ]; } + /** + * Return an OperationsClient object with the same endpoint as $this. + * + * @return \Google\GAX\LongRunning\OperationsClient + */ public function getOperationsClient() { return $this->operationsClient; } + /** + * Resume an existing long running operation that was previously started + * by a long running API method. If $methodName is not provided, or does + * not match a long running API method, then the operation can still be + * resumed, but the OperationResponse object will not deserialize the + * final response. + * + * @param string $operationName The name of the long running operation + * @param string $methodName The name of the method used to start the operation + * + * @return \Google\GAX\OperationResponse + */ + public function resumeOperation($operationName, $methodName = null) + { + $lroDescriptors = self::getLongRunningDescriptors(); + if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) { + $options = $lroDescriptors[$methodName]; + } else { + $options = []; + } + $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options); + $operation->reload(); + + return $operation; + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -302,10 +334,14 @@ public function __construct($options = []) ]; $options = array_merge($defaultOptions, $options); - $this->operationsClient = new OperationsClient([ - 'serviceAddress' => $options['serviceAddress'], - 'scopes' => $options['scopes'], - ]); + if (array_key_exists('operationsClient', $options)) { + $this->operationsClient = $options['operationsClient']; + } else { + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + ]); + } $headerDescriptor = new AgentHeaderDescriptor([ 'clientName' => $options['appName'], @@ -475,6 +511,23 @@ public function listDatabases($parent, $optionalArgs = []) * $error = $operationResponse->getError(); * // handleError($error) * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'createDatabase'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } * } finally { * $databaseAdminClient->close(); * } @@ -607,6 +660,22 @@ public function getDatabase($name, $optionalArgs = []) * $error = $operationResponse->getError(); * // handleError($error) * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'updateDatabaseDdl'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * // operation succeeded and returns no value + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } * } finally { * $databaseAdminClient->close(); * } diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php index df798a185421..bb7d0a10b257 100644 --- a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -34,6 +34,7 @@ use Google\GAX\GrpcConstants; use Google\GAX\GrpcCredentialsHelper; use Google\GAX\LongRunning\OperationsClient; +use Google\GAX\OperationResponse; use Google\GAX\PageStreamingDescriptor; use Google\GAX\PathTemplate; use google\iam\v1\GetIamPolicyRequest; @@ -294,11 +295,42 @@ private static function getLongRunningDescriptors() ]; } + /** + * Return an OperationsClient object with the same endpoint as $this. + * + * @return \Google\GAX\LongRunning\OperationsClient + */ public function getOperationsClient() { return $this->operationsClient; } + /** + * Resume an existing long running operation that was previously started + * by a long running API method. If $methodName is not provided, or does + * not match a long running API method, then the operation can still be + * resumed, but the OperationResponse object will not deserialize the + * final response. + * + * @param string $operationName The name of the long running operation + * @param string $methodName The name of the method used to start the operation + * + * @return \Google\GAX\OperationResponse + */ + public function resumeOperation($operationName, $methodName = null) + { + $lroDescriptors = self::getLongRunningDescriptors(); + if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) { + $options = $lroDescriptors[$methodName]; + } else { + $options = []; + } + $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options); + $operation->reload(); + + return $operation; + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -348,10 +380,14 @@ public function __construct($options = []) ]; $options = array_merge($defaultOptions, $options); - $this->operationsClient = new OperationsClient([ - 'serviceAddress' => $options['serviceAddress'], - 'scopes' => $options['scopes'], - ]); + if (array_key_exists('operationsClient', $options)) { + $this->operationsClient = $options['operationsClient']; + } else { + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + ]); + } $headerDescriptor = new AgentHeaderDescriptor([ 'clientName' => $options['appName'], @@ -756,6 +792,23 @@ public function getInstance($name, $optionalArgs = []) * $error = $operationResponse->getError(); * // handleError($error) * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'createInstance'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } * } finally { * $instanceAdminClient->close(); * } @@ -863,6 +916,23 @@ public function createInstance($parent, $instanceId, $instance, $optionalArgs = * $error = $operationResponse->getError(); * // handleError($error) * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $instanceAdminClient->updateInstance($instance, $fieldMask); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'updateInstance'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } * } finally { * $instanceAdminClient->close(); * } diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index 6f26b186090b..94ecc67c8350 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -223,6 +223,18 @@ private static function getSessionNameTemplate() return self::$sessionNameTemplate; } + private static function getGrpcStreamingDescriptors() + { + return [ + 'executeStreamingSql' => [ + 'grpcStreamingType' => 'ServerStreaming', + ], + 'streamingRead' => [ + 'grpcStreamingType' => 'ServerStreaming', + ], + ]; + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -287,11 +299,17 @@ public function __construct($options = []) 'getSession' => $defaultDescriptors, 'deleteSession' => $defaultDescriptors, 'executeSql' => $defaultDescriptors, + 'executeStreamingSql' => $defaultDescriptors, 'read' => $defaultDescriptors, + 'streamingRead' => $defaultDescriptors, 'beginTransaction' => $defaultDescriptors, 'commit' => $defaultDescriptors, 'rollback' => $defaultDescriptors, ]; + $grpcStreamingDescriptors = self::getGrpcStreamingDescriptors(); + foreach ($grpcStreamingDescriptors as $method => $grpcStreamingDescriptor) { + $this->descriptors[$method]['grpcStreamingDescriptor'] = $grpcStreamingDescriptor; + } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/spanner_client_config.json'); $clientConfig = json_decode($clientConfigJsonString, true); @@ -617,6 +635,118 @@ public function executeSql($session, $sql, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * Like [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], except returns the result + * set as a stream. Unlike [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], there + * is no limit on the size of the returned result set. However, no + * individual row in the result set can exceed 100 MiB, and no + * column value can exceed 10 MiB. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $sql = ""; + * // Read all responses until the stream is complete + * $streamingResponse = $spannerClient->executeStreamingSql($formattedSession, $sql); + * foreach ($streamingResponse->readAll() as $element) { + * // doSomethingWith($element); + * } + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the SQL query should be performed. + * @param string $sql Required. The SQL query string. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type Struct $params + * The SQL query string can contain parameter placeholders. A parameter + * placeholder consists of `'@'` followed by the parameter + * name. Parameter names consist of any combination of letters, + * numbers, and underscores. + * + * Parameters can appear anywhere that a literal value is expected. The same + * parameter name can be used more than once, for example: + * `"WHERE id > @msg_id AND id < @msg_id + 100"` + * + * It is an error to execute an SQL query with unbound parameters. + * + * Parameter values are specified using `params`, which is a JSON + * object whose keys are parameter names, and whose values are the + * corresponding parameter values. + * @type array $paramTypes + * It is not always possible for Cloud Spanner to infer the right SQL type + * from a JSON value. For example, values of type `BYTES` and values + * of type `STRING` both appear in [params][google.spanner.v1.ExecuteSqlRequest.params] as JSON strings. + * + * In these cases, `param_types` can be used to specify the exact + * SQL type for some or all of the SQL query parameters. See the + * definition of [Type][google.spanner.v1.Type] for more information + * about SQL types. + * @type string $resumeToken + * If this request is resuming a previously interrupted SQL query + * execution, `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new SQL query execution to resume where the last one left + * off. The rest of the request parameters must exactly match the + * request that yielded this token. + * @type QueryMode $queryMode + * Used to control the amount of debugging information returned in + * [ResultSetStats][google.spanner.v1.ResultSetStats]. + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * + * @return \Google\GAX\ServerStreamingResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function executeStreamingSql($session, $sql, $optionalArgs = []) + { + $request = new ExecuteSqlRequest(); + $request->setSession($session); + $request->setSql($sql); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['params'])) { + $request->setParams($optionalArgs['params']); + } + if (isset($optionalArgs['paramTypes'])) { + foreach ($optionalArgs['paramTypes'] as $key => $value) { + $request->addParamTypes((new ParamTypesEntry())->setKey($key)->setValue($value)); + } + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + if (isset($optionalArgs['queryMode'])) { + $request->setQueryMode($optionalArgs['queryMode']); + } + + $mergedSettings = $this->defaultCallSettings['executeStreamingSql']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'ExecuteStreamingSql', + $mergedSettings, + $this->descriptors['executeStreamingSql'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Reads rows from the database using key lookups and scans, as a * simple key/value style alternative to @@ -730,6 +860,111 @@ public function read($session, $table, $columns, $keySet, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * Like [Read][google.spanner.v1.Spanner.Read], except returns the result set as a + * stream. Unlike [Read][google.spanner.v1.Spanner.Read], there is no limit on the + * size of the returned result set. However, no individual row in + * the result set can exceed 100 MiB, and no column value can exceed + * 10 MiB. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $table = ""; + * $columns = []; + * $keySet = new KeySet(); + * // Read all responses until the stream is complete + * $streamingResponse = $spannerClient->streamingRead($formattedSession, $table, $columns, $keySet); + * foreach ($streamingResponse->readAll() as $element) { + * // doSomethingWith($element); + * } + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the read should be performed. + * @param string $table Required. The name of the table in the database to be read. + * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching + * this request. + * @param KeySet $keySet Required. `key_set` identifies the rows to be yielded. `key_set` names the + * primary keys of the rows in [table][google.spanner.v1.ReadRequest.table] to be yielded, unless [index][google.spanner.v1.ReadRequest.index] + * is present. If [index][google.spanner.v1.ReadRequest.index] is present, then [key_set][google.spanner.v1.ReadRequest.key_set] instead names + * index keys in [index][google.spanner.v1.ReadRequest.index]. + * + * Rows are yielded in table primary key order (if [index][google.spanner.v1.ReadRequest.index] is empty) + * or index key order (if [index][google.spanner.v1.ReadRequest.index] is non-empty). + * + * It is not an error for the `key_set` to name rows that do not + * exist in the database. Read yields nothing for nonexistent rows. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type string $index + * If non-empty, the name of an index on [table][google.spanner.v1.ReadRequest.table]. This index is + * used instead of the table primary key when interpreting [key_set][google.spanner.v1.ReadRequest.key_set] + * and sorting result rows. See [key_set][google.spanner.v1.ReadRequest.key_set] for further information. + * @type int $limit + * If greater than zero, only the first `limit` rows are yielded. If `limit` + * is zero, the default is no limit. + * @type string $resumeToken + * If this request is resuming a previously interrupted read, + * `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new read to resume where the last read left off. The + * rest of the request parameters must exactly match the request + * that yielded this token. + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * + * @return \Google\GAX\ServerStreamingResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function streamingRead($session, $table, $columns, $keySet, $optionalArgs = []) + { + $request = new ReadRequest(); + $request->setSession($session); + $request->setTable($table); + foreach ($columns as $elem) { + $request->addColumns($elem); + } + $request->setKeySet($keySet); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['index'])) { + $request->setIndex($optionalArgs['index']); + } + if (isset($optionalArgs['limit'])) { + $request->setLimit($optionalArgs['limit']); + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + + $mergedSettings = $this->defaultCallSettings['streamingRead']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'StreamingRead', + $mergedSettings, + $this->descriptors['streamingRead'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Begins a new transaction. This step can often be skipped: * [Read][google.spanner.v1.Spanner.Read], [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] and From 8a9ca46186e37102a9199e869b836650dd85afd6 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Thu, 19 Jan 2017 16:50:05 -0500 Subject: [PATCH 042/107] Add transaction ID to read, execute --- src/Spanner/Connection/Grpc.php | 11 +++++++++++ src/Spanner/Operation.php | 16 ++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 280c6a071783..7ecdb298728b 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -32,6 +32,7 @@ use google\spanner\v1\KeySet; use google\spanner\v1\Mutation; use google\spanner\v1\TransactionOptions; +use google\spanner\v1\TransactionSelector; use google\spanner\v1\Type; class Grpc implements ConnectionInterface @@ -367,6 +368,11 @@ public function executeSql(array $args = []) ->deserialize($param, $this->codec); } + if (isset($args['transaction'])) { + $args['transaction'] = (new TransactionSelector) + ->deserialize(['id' => $args['transaction']], $this->codec); + } + return $this->send([$this->spannerClient, 'executeSql'], [ $this->pluck('session', $args), $this->pluck('sql', $args), @@ -383,6 +389,11 @@ public function read(array $args = []) $keySet = (new KeySet) ->deserialize($this->formatKeySet($keySet), $this->codec); + if (isset($args['transaction'])) { + $args['transaction'] = (new TransactionSelector) + ->deserialize(['id' => $args['transaction']], $this->codec); + } + return $this->send([$this->spannerClient, 'read'], [ $this->pluck('session', $args), $this->pluck('table', $args), diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index bad28687b1ba..4159df01b319 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -125,20 +125,12 @@ public function commit(Session $session, Transaction $transaction, array $option $options['singleUseTransaction'] = ['readWrite' => []]; } - echo 'Committing in Session '. $session->name() . PHP_EOL; - echo 'Calling commit ' . $transaction->id() . PHP_EOL . PHP_EOL; - - echo microtime(true);echo PHP_EOL.PHP_EOL; - $res = $this->connection->commit([ 'transactionId' => $transaction->id(), 'mutations' => $transaction->mutations(), 'session' => $session->name() ] + $options); - echo 'Commit done ' . $transaction->id() . PHP_EOL . PHP_EOL; - echo microtime(true);echo PHP_EOL.PHP_EOL; - return $this->mapper->createTimestampWithNanos($res['commitTimestamp']); } @@ -176,6 +168,7 @@ public function execute(Session $session, $sql, array $options = []) { $options += [ 'parameters' => [], + 'transactionId' => null, ]; $parameters = $this->pluck('parameters', $options); @@ -183,7 +176,8 @@ public function execute(Session $session, $sql, array $options = []) $res = $this->connection->executeSql([ 'sql' => $sql, - 'session' => $session->name() + 'session' => $session->name(), + 'transactionId' => $options['transactionId'] ] + $options); return $this->createResult($res); @@ -212,6 +206,7 @@ public function read(Session $session, $table, array $options = []) 'keySet' => null, 'offset' => null, 'limit' => null, + 'transactionId' => null, ]; if (is_null($options['keySet'])) { @@ -225,7 +220,8 @@ public function read(Session $session, $table, array $options = []) $res = $this->connection->read([ 'table' => $table, - 'session' => $session->name() + 'session' => $session->name(), + 'transaction' => $options['transactionId'] ] + $options); return $this->createResult($res); From c18f58b99c9127e4f571d5feef297c5720f5f5d9 Mon Sep 17 00:00:00 2001 From: Michael Bausor Date: Fri, 20 Jan 2017 14:04:06 -0800 Subject: [PATCH 043/107] Regenerate spanner with updated comments --- composer.json | 2 +- src/Spanner/V1/SpannerClient.php | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index 5606dbc21c5b..cfb15605d8a9 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", "google/proto-client-php": "^0.5", - "google/gax": "^0.3" + "google/gax": "^0.7" }, "suggest": { "google/gax": "Required to support gRPC", diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index 94ecc67c8350..a88419ad1441 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -649,8 +649,8 @@ public function executeSql($session, $sql, $optionalArgs = []) * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $sql = ""; * // Read all responses until the stream is complete - * $streamingResponse = $spannerClient->executeStreamingSql($formattedSession, $sql); - * foreach ($streamingResponse->readAll() as $element) { + * $stream = $spannerClient->executeStreamingSql($formattedSession, $sql); + * foreach ($stream->readAll() as $element) { * // doSomethingWith($element); * } * } finally { @@ -876,8 +876,8 @@ public function read($session, $table, $columns, $keySet, $optionalArgs = []) * $columns = []; * $keySet = new KeySet(); * // Read all responses until the stream is complete - * $streamingResponse = $spannerClient->streamingRead($formattedSession, $table, $columns, $keySet); - * foreach ($streamingResponse->readAll() as $element) { + * $stream = $spannerClient->streamingRead($formattedSession, $table, $columns, $keySet); + * foreach ($stream->readAll() as $element) { * // doSomethingWith($element); * } * } finally { From 4420d1096afe3bd7c07683a678a626c6c7998a93 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 25 Jan 2017 12:43:42 -0500 Subject: [PATCH 044/107] Add snapshot, retry transactions, read in transaction --- composer.json | 6 +- src/Exception/AbortedException.php | 42 +++++++ src/Exception/ServiceException.php | 14 ++- src/GrpcRequestWrapper.php | 33 ++++- src/GrpcTrait.php | 20 +++ src/Retry.php | 106 ++++++++++++++++ src/Spanner/Connection/Grpc.php | 16 ++- src/Spanner/Database.php | 139 +++++++++++++++++---- src/Spanner/Operation.php | 112 +++++++++-------- src/Spanner/ReaderInterface.php | 45 +++++++ src/Spanner/Result.php | 19 +++ src/Spanner/Snapshot.php | 78 ++++++++++++ src/Spanner/Transaction.php | 187 +++++------------------------ src/Spanner/TransactionBase.php | 139 +++++++++++++++++++++ 14 files changed, 721 insertions(+), 235 deletions(-) create mode 100644 src/Exception/AbortedException.php create mode 100644 src/Retry.php create mode 100644 src/Spanner/ReaderInterface.php create mode 100644 src/Spanner/Snapshot.php create mode 100644 src/Spanner/TransactionBase.php diff --git a/composer.json b/composer.json index 5d3a4305e8c9..daa5362ce2f5 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", "google/proto-client-php": "dev-master", - "google/gax": "^0.5" + "google/gax": "dev-streaming" }, "suggest": { "google/gax": "Required to support gRPC", @@ -81,6 +81,10 @@ { "type": "vcs", "url": "https://github.com/jdpedrie/proto-client-php-private" + }, + { + "type": "vcs", + "url": "https://github.com/michaelbausor/gax-php" } ] } diff --git a/src/Exception/AbortedException.php b/src/Exception/AbortedException.php new file mode 100644 index 000000000000..593b6222a465 --- /dev/null +++ b/src/Exception/AbortedException.php @@ -0,0 +1,42 @@ +options, function ($metadataItem) { + if (array_key_exists('retryDelay', $metadataItem)) { + return true; + } + + return false; + }); + + $delay = $metadata[0]['retryDelay']; + if (!isset($delay['seconds'])) { + $delay['seconds'] = 0; + } + + return $delay; + } +} diff --git a/src/Exception/ServiceException.php b/src/Exception/ServiceException.php index 87ae111eac19..eab79e38b58f 100644 --- a/src/Exception/ServiceException.php +++ b/src/Exception/ServiceException.php @@ -29,6 +29,11 @@ class ServiceException extends GoogleException */ private $serviceException; + /** + * @var array + */ + protected $options; + /** * Handle previous exceptions differently here. * @@ -36,9 +41,14 @@ class ServiceException extends GoogleException * @param int $code * @param Exception $serviceException */ - public function __construct($message, $code = null, Exception $serviceException = null) - { + public function __construct( + $message, + $code = null, + Exception $serviceException = null, + array $options = [] + ) { $this->serviceException = $serviceException; + $this->options = $options; parent::__construct($message, $code); } diff --git a/src/GrpcRequestWrapper.php b/src/GrpcRequestWrapper.php index 4d73e671f023..5d2ffd9628ea 100644 --- a/src/GrpcRequestWrapper.php +++ b/src/GrpcRequestWrapper.php @@ -17,6 +17,7 @@ namespace Google\Cloud; +use DrSlump\Protobuf\Codec\Binary; use DrSlump\Protobuf\Codec\CodecInterface; use DrSlump\Protobuf\Message; use Google\Auth\FetchAuthTokenInterface; @@ -47,6 +48,11 @@ class GrpcRequestWrapper */ private $codec; + /** + * @var CodecInterface A codec used for binary deserialization. + */ + private $binaryCodec; + /** * @var array gRPC specific configuration options passed off to the GAX * library. @@ -63,6 +69,13 @@ class GrpcRequestWrapper Grpc\STATUS_DATA_LOSS ]; + /** + * @var array Map of error metadata types to RPC wrappers. + */ + private $metadataTypes = [ + 'google.rpc.retryinfo-bin' => \google\rpc\RetryInfo::class + ]; + /** * @param array $config [optional] { * Configuration options. Please see @@ -88,6 +101,7 @@ public function __construct(array $config = []) $this->authHttpHandler = $config['authHttpHandler'] ?: HttpHandlerFactory::build(); $this->codec = $config['codec']; $this->grpcOptions = $config['grpcOptions']; + $this->binaryCodec = new Binary; } /** @@ -184,11 +198,28 @@ private function convertToGoogleException(ApiException $ex) $exception = Exception\ServerException::class; break; + case Grpc\STATUS_ABORTED: + $exception = Exception\AbortedException::class; + break; + default: $exception = Exception\ServiceException::class; break; } - return new $exception($ex->getMessage(), $ex->getCode(), $ex); + $metadata = []; + if ($ex->getMetadata()) { + foreach ($ex->getMetadata() as $type => $binaryValue) { + if (!isset($this->metadataTypes[$type])) { + continue; + } + + $metadata[] = (new $this->metadataTypes[$type]) + ->deserialize($binaryValue[0], $this->binaryCodec) + ->serialize($this->codec); + } + } + + return new $exception($ex->getMessage(), $ex->getCode(), $ex, $metadata); } } diff --git a/src/GrpcTrait.php b/src/GrpcTrait.php index ffc0fcd6136e..65d6b2527b63 100644 --- a/src/GrpcTrait.php +++ b/src/GrpcTrait.php @@ -178,4 +178,24 @@ private function formatValueForApi($value) return ['list_value' => $this->formatListForApi($value)]; } } + + /** + * Format a timestamp for the API with nanosecond precision. + * + * @param string $value + * @return array + */ + private function formatTimestampForApi($value) + { + preg_match('/\.(\d{1,9})Z/', $value, $matches); + $value = preg_replace('/\.(\d{1,9})Z/', '.000000Z', $value); + + $dt = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', $value); + $nanos = (isset($matches[1])) ? $matches[1] : 0; + + return [ + 'seconds' => (int)$dt->format('U'), + 'nanos' => (int)$nanos + ]; + } } diff --git a/src/Retry.php b/src/Retry.php new file mode 100644 index 000000000000..94167fb2b6fa --- /dev/null +++ b/src/Retry.php @@ -0,0 +1,106 @@ + (int >= 0), 'nanos' => (int >= 0)] specifying how + * long an operation should pause before retrying. Should accept a + * single argument of type `\Exception`. + * @param callable $retryFunction [optional] returns bool for whether or not + * to retry. + */ + public function __construct( + $retries, + callable $delayFunction, + callable $retryFunction = null + ) { + $this->retries = $retries !== null ? (int) $retries : 3; + $this->retryFunction = $retryFunction; + $this->delayFunction = $delayFunction; + } + + /** + * Executes the retry process. + * + * @param callable $function + * @param array $arguments [optional] + * @return mixed + * @throws \Exception The last exception caught while retrying. + */ + public function execute(callable $function, array $arguments = []) + { + $delayFunction = $this->delayFunction; + $retryAttempt = 0; + $exception = null; + + while (true) { + try { + return call_user_func_array($function, $arguments); + } catch (\Exception $exception) { + if ($this->retryFunction) { + if (!call_user_func($this->retryFunction, $exception)) { + throw $exception; + } + } + + if ($retryAttempt >= $this->retries) { + break; + } + + $delayFunction($exception); + $retryAttempt++; + } + } + + throw $exception; + } + + /** + * @param callable $delayFunction + * @return void + */ + public function setDelayFunction(callable $delayFunction) + { + $this->delayFunction = $delayFunction; + } +} diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 7ecdb298728b..2cb5317c8500 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -368,9 +368,9 @@ public function executeSql(array $args = []) ->deserialize($param, $this->codec); } - if (isset($args['transaction'])) { + if (isset($args['transactionId'])) { $args['transaction'] = (new TransactionSelector) - ->deserialize(['id' => $args['transaction']], $this->codec); + ->deserialize(['id' => $args['transactionId']], $this->codec); } return $this->send([$this->spannerClient, 'executeSql'], [ @@ -411,8 +411,18 @@ public function beginTransaction(array $args = []) $options = new TransactionOptions; if (isset($args['transactionOptions']['readOnly'])) { + $ro = $args['transactionOptions']['readOnly']; + + if (isset($ro['minReadTimestamp'])) { + $ro['minReadTimestamp'] = $this->formatTimestampForApi($ro['minReadTimestamp']); + } + + if (isset($ro['readTimestamp'])) { + $ro['readTimestamp'] = $this->formatTimestampForApi($ro['readTimestamp']); + } + $readOnly = (new TransactionOptions\ReadOnly) - ->deserialize($args['transactionOptions']['readOnly'], $this->codec); + ->deserialize($ro, $this->codec); $options->setReadOnly($readOnly); } else { diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 750635b15192..13485ecd6735 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -18,8 +18,10 @@ namespace Google\Cloud\Spanner; use Google\Cloud\ArrayTrait; +use Google\Cloud\Exception\AbortedException; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; +use Google\Cloud\Retry; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Google\Cloud\Spanner\Session\SessionPoolInterface; @@ -49,10 +51,12 @@ * $database = $instance->database('my-database'); * ``` */ -class Database +class Database implements ReaderInterface { use ArrayTrait; + const MAX_RETRIES = 3; + /** * @var ConnectionInterface */ @@ -307,14 +311,25 @@ public function iam() } /** - * Execute read operations inside a transaction. + * Create a snapshot to read from a database at a point in time. * * If no configuration options are provided, transaction will be opened with * strong consistency. * + * Snapshots are executed behind the scenes using a Read-Only Transaction. + * * Example: * ``` - * $transaction = $database->readOnlyTransaction(); + * $snapshot = $database->snapshot(); + * ``` + * + * ``` + * // Take a shapshot with a returned timestamp. + * $snapshot = $database->snapshot([ + * 'returnReadTimestamp' => true + * ]); + * + * $timestamp = $snapshot->readTimestamp(); * ``` * * @codingStandardsIgnoreStart @@ -349,7 +364,7 @@ public function iam() * @codingStandardsIgnoreEnd * @return Transaction */ - public function readOnlyTransaction(callable $operation, array $options = []) + public function snapshot(array $options = []) { $options += [ 'returnReadTimestamp' => null, @@ -396,17 +411,36 @@ public function readOnlyTransaction(callable $operation, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); - $transaction = $this->operation->transaction($session, SessionPoolInterface::CONTEXT_READ, $options); - - return call_user_func($operation, $transaction); + return $this->operation->snapshot($session, $options); } /** * Execute Read/Write operations inside a Transaction. * + * Using this method and providing a callable operation provides certain + * benefits including automatic retry when a transaction fails. In case of a + * failure, all transaction operations, including reads, are re-applied in a + * new transaction. + * * Example: * ``` - * $transaction = $database->readWriteTransaction(); + * $transaction = $database->runTransaction(function (Transaction $t) use ($userName, $password) { + * $user = $t->execute('SELECT * FROM Users WHERE Name = @name and PasswordHash = @password', [ + * 'parameters' => [ + * 'name' => $userName, + * 'password' => password_hash($password) + * ] + * ])->firstRow(); + * + * if ($user) { + * grantAccess($user); + * + * $user['loginCount'] = $user['loginCount'] + 1; + * $t->update('Users', $user); + * } + * + * $t->commit(); + * }); * ``` * * @codingStandardsIgnoreStart @@ -415,22 +449,84 @@ public function readOnlyTransaction(callable $operation, array $options = []) * * @param callable $operation The operations to run in the transaction. * **Signature:** `function (Transaction $transaction)`. - * @param array $options [optional] Configuration Options + * @param array $options [optional] { + * Configuration Options + * + * @type int $maxRetries The number of times to attempt to apply the + * operation before failing. **Defaults to ** `4`. + * } * @return Transaction */ - public function readWriteTransaction(callable $operation, array $options = []) + public function runTransaction(callable $operation, array $options = []) { + $options += [ + 'maxRetries' => self::MAX_RETRIES + ]; + + // There isn't anything configurable here. $options['transactionOptions'] = [ 'readWrite' => [] ]; $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - $transaction = $this->operation->transaction($session, SessionPoolInterface::CONTEXT_READWRITE, $options); + $startTransactionFn = function ($session, $options) { + return $this->operation->transaction($session, $options); + }; - call_user_func($operation, $transaction); + $delayFn = function (\Exception $e) { + if (!($e instanceof AbortedException)) { + throw $e; + } - // return $this->operation->commit($session, $transaction, $options); + return $e->getRetryDelay(); + }; + + $commitFn = function($operation, $session, $options) use ($startTransactionFn) { + + $transaction = call_user_func_array($startTransactionFn, [ + $session, + $options + ]); + + return call_user_func($operation, $transaction); + }; + + $retry = new Retry($options['maxRetries'], $delayFn); + return $retry->execute($commitFn, [$operation, $session, $options]); + } + + /** + * Create and return a new Transaction. + * + * When manually using a Transaction, it is advised that retry logic be + * implemented to reapply all operations when an instance of + * {@see Google\Cloud\Exception\AbortedException} is thrown. + * + * If you wish Google Cloud PHP to handle retry logic for you (recommended + * for most cases), use {@see Google\Cloud\Spanner\Database::runTransaction()}. + * + * Example: + * ``` + * $transaction = $database->transaction(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @codingStandardsIgnoreEnd + * + * @param array $options [optional] Configuration Options. + * @return Transaction + */ + public function transaction(array $options = []) + { + // There isn't anything configurable here. + $options['transactionOptions'] = [ + 'readWrite' => [] + ]; + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + return $this->operation->transaction($session, $options); } /** @@ -794,31 +890,30 @@ public function execute($sql, array $options = []) * 'keys' => [1337] * ]); * - * $result = $database->read('Posts', [ - * 'keySet' => $keySet - * ]); + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns); * ``` * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest ReadRequest * - * @codingStandardsIgnoreStart * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. * @param array $options [optional] { * Configuration Options. * * @type string $index The name of an index on the table. - * @type array $columns A list of column names to be returned. - * @type KeySet $keySet A KeySet defining which rows to return. * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. * } - * @codingStandardsIgnoreEnd + * @return Result */ - public function read($table, array $options = []) + public function read($table, KeySet $keySet, array $columns, array $options = []) { $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); - return $this->operation->read($session, $table, $options); + return $this->operation->read($session, $table, $keySet, $columns, $options); } /** diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 4159df01b319..ffd1c618bc19 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -112,20 +112,16 @@ public function deleteMutation($table, KeySet $keySet) * @codingStandardsIgnoreStart * @param Session $session The session ID to use for the commit. * @param Transaction $transaction The transaction to commit. - * @param array $options [optional] Configuration options. + * @param array $options [optional] { + * Configuration options. + * + * @type int $maxRetries + * } * @return Timestamp The commit Timestamp. */ public function commit(Session $session, Transaction $transaction, array $options = []) { - if ($transaction->context() !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new \RuntimeException('Cannot commit in a Read-Only Transaction'); - } - - if (!isset($options['transactionId'])) { - $options['singleUseTransaction'] = ['readWrite' => []]; - } - - $res = $this->connection->commit([ + return $this->connection->commit([ 'transactionId' => $transaction->id(), 'mutations' => $transaction->mutations(), 'session' => $session->name() @@ -146,12 +142,8 @@ public function commit(Session $session, Transaction $transaction, array $option */ public function rollback(Session $session, Transaction $transaction, array $options = []) { - if ($transaction->context() !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new \RuntimeException('Cannot rollback a Read-Only Transaction'); - } - return $this->connection->rollback([ - 'transactionId' => $transactionId, + 'transactionId' => $transaction->id(), 'session' => $session->name() ] + $options); } @@ -173,11 +165,9 @@ public function execute(Session $session, $sql, array $options = []) $parameters = $this->pluck('parameters', $options); $options += $this->mapper->formatParamsForExecuteSql($parameters); - $res = $this->connection->executeSql([ 'sql' => $sql, - 'session' => $session->name(), - 'transactionId' => $options['transactionId'] + 'session' => $session->name() ] + $options); return $this->createResult($res); @@ -187,73 +177,95 @@ public function execute(Session $session, $sql, array $options = []) * Lookup rows in a database. * * @param Session $session The session in which to read data. - * @param string $table The table to read from. + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. * @param array $options [optional] { - * Configuration Options + * Configuration Options. * - * @type string $index - * @type array $columns - * @type KeySet $keySet - * @type string $offset - * @type int $limit + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. * } + * @return Result */ - public function read(Session $session, $table, array $options = []) + public function read(Session $session, $table, KeySet $keySet, array $columns, array $options = []) { $options += [ 'index' => null, - 'columns' => [], - 'keySet' => null, - 'offset' => null, 'limit' => null, + 'offset' => null, 'transactionId' => null, ]; - if (is_null($options['keySet'])) { - $options['keySet'] = new KeySet(); - $options['keySet']->setMatchAll(true); - } elseif (!($options['keySet'] instanceof KeySet)) { - throw new \InvalidArgumentException('$options.keySet must be an instance of KeySet'); - } - - $options['keySet'] = $this->flattenKeySet($options['keySet']); - $res = $this->connection->read([ 'table' => $table, 'session' => $session->name(), - 'transaction' => $options['transactionId'] + 'transaction' => $options['transactionId'], + 'columns' => $columns, + 'keySet' => $this->flattenKeySet($keySet) ] + $options); return $this->createResult($res); } /** - * Create a transaction with a given context. + * Create a read/write transaction. + * + * @todo if a transaction is already available on the session, get it instead + * of starting a new one? * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest * * @param Session $session The session to start the transaction in. - * @param string $context The context of the new transaction. * @param array $options [optional] Configuration options. * @return Transaction */ - public function transaction(Session $session, $context, array $options = []) + public function transaction(Session $session, array $options = []) { - $options += [ - 'transactionOptions' => [] - ]; + $res = $this->beginTransaction($session, $options); + return new Transaction($this, $session, $res['id']); + } - // make a service call here. - $res = $this->connection->beginTransaction($options + [ - 'session' => $session->name(), - ]); + /** + * Create a read-only snapshot transaction. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the snapshot in. + * @param array $options [optional] Configuration options. + * @return Snapshot + */ + public function snapshot(Session $session, array $options = []) + { + $res = $this->beginTransaction($session, $options); $timestamp = null; if (isset($res['readTimestamp'])) { $timestamp = $this->mapper->createTimestampWithNanos($res['readTimestamp']); } - return new Transaction($this, $session, $context, $res['id'], $timestamp); + return new Snapshot($this, $session, $res['id'], $timestamp); + } + + /** + * Execute a service call to begin a transaction or snapshot. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the snapshot in. + * @param array $options [optional] Configuration options. + * @return array + */ + private function beginTransaction(Session $session, array $options = []) + { + $options += [ + 'transactionOptions' => [] + ]; + + return $this->connection->beginTransaction($options + [ + 'session' => $session->name(), + ]); } /** diff --git a/src/Spanner/ReaderInterface.php b/src/Spanner/ReaderInterface.php new file mode 100644 index 000000000000..74bba4a7a36b --- /dev/null +++ b/src/Spanner/ReaderInterface.php @@ -0,0 +1,45 @@ +rows; } + /** + * Return the first row, or null. + * + * Useful when selecting a single row. + * + * Example: + * ``` + * $row = $result->firstRow(); + * ``` + * + * @return array|null + */ + public function firstRow() + { + return (isset($this->rows[0])) + ? $this->rows[0] + : null; + } + /** * Get the query plan and execution statistics for the query that produced * this result set. diff --git a/src/Spanner/Snapshot.php b/src/Spanner/Snapshot.php new file mode 100644 index 000000000000..a5f7b0311c02 --- /dev/null +++ b/src/Spanner/Snapshot.php @@ -0,0 +1,78 @@ +spanner(); + * + * $database = $spanner->connect('my-instance', 'my-database'); + * $snapshot = $database->snapshot(); + * ``` + */ +class Snapshot extends TransactionBase +{ + /** + * @var Timestamp + */ + private $readTimestamp; + + /** + * @param Operation $operation The Operation instance. + * @param Session $session The session to use for spanner interactions. + * @param string $transactionId The Transaction ID. + * @param Timestamp $readTimestamp [optional] The read timestamp. + */ + public function __construct( + Operation $operation, + Session $session, + $transactionId, + Timestamp $readTimestamp = null + ) { + $this->operation = $operation; + $this->session = $session; + $this->transactionId = $transactionId; + $this->readTimestamp = $readTimestamp; + } + + /** + * Retrieve the Read Timestamp. + * + * For snapshot read-only transactions, the read timestamp chosen for the + * transaction. + * + * Example: + * ``` + * $timestamp = $transaction->readTimestamp(); + * ``` + * + * @return Timestamp + */ + public function readTimestamp() + { + return $this->readTimestamp; + } +} diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 7e6098fb45ef..819b98b89e10 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -24,6 +24,24 @@ /** * Manages interaction with Google Cloud Spanner inside a Transaction. * + * Transactions can be started via + * {@see Google\Cloud\Spanner\Database::runTransaction()} (recommended) or via + * {@see Google\Cloud\Spanner\Database::transaction()}. Transactions should + * always call {@see Google\Cloud\Spanner\Transaction::commit()} or + * {@see Google\Cloud\Spanner\Transaction::rollback()} to ensure that locks are + * released in a timely manner. + * + * If you do not plan on performing any writes in your transaction, a + * {@see Google\Cloud\Spanner\Snapshot} is a better solution which does not + * require a commit or rollback and does not lock any data. + * + * Transactions may raise {@see Google\Cloud\Exception\AbortedException} errors + * when the transaction cannot complete for any reason. In this case, the entire + * operation (all reads and writes) should be reapplied atomically. Google Cloud + * PHP handles this transparently when using + * {@see Google\Cloud\Spanner\Database::runTransaction()}. In other cases, it is + * highly recommended that applications implement their own retry logic. + * * Example: * ``` * use Google\Cloud\ServiceBuilder; @@ -32,40 +50,25 @@ * $spanner = $cloud->spanner(); * * $database = $spanner->connect('my-instance', 'my-database'); - * $transaction = $database->readWriteTransaction(); + * + * $database->runTransaction(function (Transaction $t) { + * // do stuff. + * + * $t->commit(); + * }); + * ``` + * + * ``` + * // Get a transaction to manage manually. + * $transaction = $database->transaction(); * ``` */ -class Transaction +class Transaction extends TransactionBase { const STATE_ACTIVE = 0; const STATE_ROLLED_BACK = 1; const STATE_COMMITTED = 2; - /** - * @var Operation - */ - private $operation; - - /** - * @var Session - */ - private $session; - - /** - * @var string - */ - private $context; - - /** - * @var string - */ - private $transactionId; - - /** - * @var Timestamp - */ - private $readTimestamp; - /** * @var array */ @@ -79,22 +82,16 @@ class Transaction /** * @param Operation $operation The Operation instance. * @param Session $session The session to use for spanner interactions. - * @param string $context The Transaction context. * @param string $transactionId The Transaction ID. - * @param Timestamp $readTimestamp [optional] The read timestamp. */ public function __construct( Operation $operation, Session $session, - $context, - $transactionId, - Timestamp $readTimestamp = null + $transactionId ) { $this->operation = $operation; $this->session = $session; - $this->context = $context; $this->transactionId = $transactionId; - $this->readTimestamp = $readTimestamp; } /** @@ -304,74 +301,6 @@ public function delete($table, KeySet $keySet) return $this; } - /** - * Run a query. - * - * Example: - * ``` - * $result = $transaction->execute( - * 'SELECT * FROM Users WHERE id = @userId', - * [ - * 'parameters' => [ - * 'userId' => 1 - * ] - * ] - * ); - * ``` - * @param string $sql The query string to execute. - * @param array $options [optional] { - * Configuration options. - * - * @type array $parameters A key/value array of Query Parameters, where - * the key is represented in the query string prefixed by a `@` - * symbol. - * } - * @return Result - */ - public function execute($sql, array $options = []) - { - return $this->operation->execute($this->session, $sql, [ - 'transactionId' => $this->transactionId - ] + $options); - } - - /** - * Lookup rows in a table. - * - * Note that if no KeySet is specified, all rows in a table will be - * returned. - * - * Example: - * ``` - * $keySet = $spanner->keySet([ - * 'keys' => [10] - * ]); - * - * $result = $database->read('Posts', [ - * 'keySet' => $keySet - * ]); - * ``` - * - * @codingStandardsIgnoreStart - * @param string $table The table name. - * @param array $options [optional] { - * Configuration Options. - * - * @type string $index The name of an index on the table. - * @type array $columns A list of column names to be returned. - * @type KeySet $keySet A KeySet, defining which rows to return. - * @type int $offset The number of rows to offset results by. - * @type int $limit The number of results to return. - * } - * @codingStandardsIgnoreEnd - */ - public function read($table, array $options = []) - { - return $this->operation->read($this->session, $table, [ - 'transactionId' => $this->transactionId - ] + $options); - } - /** * Roll back a transaction. * @@ -413,54 +342,6 @@ public function commit() return $this->operation->commit($this->session, $this); } - /** - * Retrieve the Read Timestamp. - * - * For snapshot read-only transactions, the read timestamp chosen for the - * transaction. - * - * Example: - * ``` - * $timestamp = $transaction->readTimestamp(); - * ``` - * - * @return Timestamp - */ - public function readTimestamp() - { - return $this->readTimestamp; - } - - /** - * Retrieve the Transaction ID. - * - * Example: - * ``` - * $id = $transaction->id(); - * ``` - * - * @return string - */ - public function id() - { - return $this->transactionId; - } - - /** - * Retrieve the Transaction Context - * - * Example: - * ``` - * $context = $transaction->context(); - * ``` - * - * @return string - */ - public function context() - { - return $this->context; - } - /** * Retrieve the Transaction State. * @@ -499,12 +380,6 @@ public function mutations() */ private function enqueue($op, $table, array $dataSet) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } - foreach ($dataSet as $data) { if ($op === Operation::OP_DELETE) { $this->mutations[] = $this->operation->deleteMutation($table, $data); diff --git a/src/Spanner/TransactionBase.php b/src/Spanner/TransactionBase.php new file mode 100644 index 000000000000..c6975da6389a --- /dev/null +++ b/src/Spanner/TransactionBase.php @@ -0,0 +1,139 @@ +execute( + * 'SELECT * FROM Users WHERE id = @userId', + * [ + * 'parameters' => [ + * 'userId' => 1 + * ] + * ] + * ); + * ``` + * @param string $sql The query string to execute. + * @param array $options [optional] { + * Configuration options. + * + * @type array $parameters A key/value array of Query Parameters, where + * the key is represented in the query string prefixed by a `@` + * symbol. + * } + * @return Result + */ + public function execute($sql, array $options = []) + { + return $this->operation->execute($this->session, $sql, [ + 'transactionId' => $this->transactionId + ] + $options); + } + + /** + * Lookup rows in a table. + * + * Note that if no KeySet is specified, all rows in a table will be + * returned. + * + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [10] + * ]); + * + * $result = $database->read('Posts', [ + * 'keySet' => $keySet + * ]); + * ``` + * + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * } + * @return Result + */ + public function read($table, KeySet $keySet, array $columns, array $options = []) + { + return $this->operation->read($this->session, $table, $keySet, $columns, [ + 'transactionId' => $this->transactionId + ] + $options); + } + + /** + * Retrieve the Read Timestamp. + * + * For snapshot read-only transactions, the read timestamp chosen for the + * transaction. + * + * Example: + * ``` + * $timestamp = $transaction->readTimestamp(); + * ``` + * + * @return Timestamp + */ + public function readTimestamp() + { + return $this->readTimestamp; + } + + /** + * Retrieve the Transaction ID. + * + * Example: + * ``` + * $id = $transaction->id(); + * ``` + * + * @return string + */ + public function id() + { + return $this->transactionId; + } +} From a68b57f38cae8af3ec9363313bcbb3f749ca6508 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 25 Jan 2017 14:52:54 -0500 Subject: [PATCH 045/107] Remove interface, replace abstract class with trait --- src/Spanner/Database.php | 2 +- src/Spanner/ReaderInterface.php | 45 ------------------- src/Spanner/Snapshot.php | 4 +- src/Spanner/Transaction.php | 4 +- ...ctionBase.php => TransactionReadTrait.php} | 8 ++-- 5 files changed, 11 insertions(+), 52 deletions(-) delete mode 100644 src/Spanner/ReaderInterface.php rename src/Spanner/{TransactionBase.php => TransactionReadTrait.php} (96%) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 13485ecd6735..2aac5ad64f16 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -51,7 +51,7 @@ * $database = $instance->database('my-database'); * ``` */ -class Database implements ReaderInterface +class Database { use ArrayTrait; diff --git a/src/Spanner/ReaderInterface.php b/src/Spanner/ReaderInterface.php deleted file mode 100644 index 74bba4a7a36b..000000000000 --- a/src/Spanner/ReaderInterface.php +++ /dev/null @@ -1,45 +0,0 @@ -snapshot(); * ``` */ -class Snapshot extends TransactionBase +class Snapshot { + use TransactionReadTrait; + /** * @var Timestamp */ diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 819b98b89e10..9060e9d1b28d 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -63,8 +63,10 @@ * $transaction = $database->transaction(); * ``` */ -class Transaction extends TransactionBase +class Transaction { + use TransactionReadTrait; + const STATE_ACTIVE = 0; const STATE_ROLLED_BACK = 1; const STATE_COMMITTED = 2; diff --git a/src/Spanner/TransactionBase.php b/src/Spanner/TransactionReadTrait.php similarity index 96% rename from src/Spanner/TransactionBase.php rename to src/Spanner/TransactionReadTrait.php index c6975da6389a..354f2dd1d903 100644 --- a/src/Spanner/TransactionBase.php +++ b/src/Spanner/TransactionReadTrait.php @@ -20,22 +20,22 @@ /** * Shared methods for reads inside a transaction. */ -abstract class TransactionBase implements ReaderInterface +trait TransactionReadTrait { /** * @var Operation */ - protected $operation; + private $operation; /** * @var Session */ - protected $session; + private $session; /** * @var string */ - protected $transactionId; + private $transactionId; /** * Run a query. From 0aa1c452ebaabd5dde05b3ce37042cdca08cc599 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Thu, 26 Jan 2017 14:37:08 -0500 Subject: [PATCH 046/107] Support single use and begin transaction --- src/ArrayTrait.php | 4 +- src/Spanner/Connection/Grpc.php | 32 ++-- src/Spanner/Database.php | 147 ++++++++++------ src/Spanner/Duration.php | 120 +++++++++++++ src/Spanner/Operation.php | 88 +++++++--- src/Spanner/Result.php | 23 ++- src/Spanner/Session/Session.php | 11 ++ src/Spanner/Snapshot.php | 2 + src/Spanner/SpannerClient.php | 5 + src/Spanner/Transaction.php | 6 +- src/Spanner/TransactionConfigurationTrait.php | 157 ++++++++++++++++++ src/Spanner/TransactionReadTrait.php | 33 +++- 12 files changed, 530 insertions(+), 98 deletions(-) create mode 100644 src/Spanner/Duration.php create mode 100644 src/Spanner/TransactionConfigurationTrait.php diff --git a/src/ArrayTrait.php b/src/ArrayTrait.php index eea200bf2b14..4977863f7dc7 100644 --- a/src/ArrayTrait.php +++ b/src/ArrayTrait.php @@ -80,12 +80,12 @@ private function isAssoc(array $arr) } /** - * Just like array_filter(), but preserves boolean values. + * Just like array_filter(), but preserves falsey values except null. * * @param array $arr * @return array */ - private function arrayFilterPreserveBool(array $arr) + private function arrayFilterRemoveNull(array $arr) { return array_filter($arr, function ($element) { if (is_bool($element)) { diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 2cb5317c8500..5ffc842f9362 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -360,18 +360,19 @@ public function deleteSession(array $args = []) */ public function executeSql(array $args = []) { - $args['params'] = (new protobuf\Struct) - ->deserialize($this->formatStructForApi($args['params']), $this->codec); + $params = new protobuf\Struct; + if (!empty($args['params'])) { + $params->deserialize($this->formatStructForApi($args['params']), $this->codec); + } + + $args['params'] = $params; foreach ($args['paramTypes'] as $key => $param) { $args['paramTypes'][$key] = (new Type) ->deserialize($param, $this->codec); } - if (isset($args['transactionId'])) { - $args['transaction'] = (new TransactionSelector) - ->deserialize(['id' => $args['transactionId']], $this->codec); - } + $args['transaction'] = $this->createTransactionSelector($args); return $this->send([$this->spannerClient, 'executeSql'], [ $this->pluck('session', $args), @@ -389,10 +390,7 @@ public function read(array $args = []) $keySet = (new KeySet) ->deserialize($this->formatKeySet($keySet), $this->codec); - if (isset($args['transaction'])) { - $args['transaction'] = (new TransactionSelector) - ->deserialize(['id' => $args['transaction']], $this->codec); - } + $args['transaction'] = $this->createTransactionSelector($args); return $this->send([$this->spannerClient, 'read'], [ $this->pluck('session', $args), @@ -403,6 +401,18 @@ public function read(array $args = []) ]); } + private function createTransactionSelector(array $args) + { + $selector = new TransactionSelector; + if (isset($args['transaction'])) { + $selector = $selector->deserialize($this->pluck('transaction', $args), $this->codec); + } elseif (isset($args['transactionId'])) { + $selector = $selector->deserialize(['id' => $this->pluck('transactionId', $args)], $this->codec); + } + + return $selector; + } + /** * @param array $args [optional] */ @@ -482,7 +492,7 @@ public function commit(array $args = []) if (isset($args['singleUseTransaction'])) { $readWrite = (new TransactionOptions\ReadWrite) - ->deserialize($args['singleUseTransaction']['readWrite'], $this->codec); + ->deserialize([], $this->codec); $options = new TransactionOptions; $options->setReadWrite($readWrite); diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 2aac5ad64f16..d4f2456382d9 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -53,7 +53,7 @@ */ class Database { - use ArrayTrait; + use TransactionConfigurationTrait; const MAX_RETRIES = 3; @@ -260,7 +260,7 @@ public function updateDdlBatch(array $statements, array $options = []) */ public function drop(array $options = []) { - return $this->connection->dropDatabase($options + [ + $this->connection->dropDatabase($options + [ 'name' => $this->fullyQualifiedDatabaseName() ]); } @@ -351,67 +351,28 @@ public function iam() * describes the transaction. * @type bool $strong Read at a timestamp where all previously committed * transactions are visible. - * @type Timestamp $minReadTimestamp Executes all reads at a timestamp - * greater than or equal to the given timestamp. - * @type int $maxStaleness Represents a number of seconds. Read data at - * a timestamp greater than or equal to the current time minus the - * given number of seconds. * @type Timestamp $readTimestamp Executes all reads at the given * timestamp. - * @type int $exactStaleness Represents a number of seconds. Executes + * @type Duration $exactStaleness Represents a number of seconds. Executes * all reads at a timestamp that is $exactStaleness old. * } * @codingStandardsIgnoreEnd - * @return Transaction + * @return Snapshot */ public function snapshot(array $options = []) { - $options += [ - 'returnReadTimestamp' => null, - 'strong' => null, - 'minReadTimestamp' => null, - 'maxStaleness' => null, - 'readTimestamp' => null, - 'exactStaleness' => null - ]; - - $options['transactionOptions'] = [ - 'readOnly' => $this->arrayFilterPreserveBool([ - 'returnReadTimestamp' => $this->pluck('returnReadTimestamp', $options), - 'strong' => $this->pluck('strong', $options), - 'minReadTimestamp' => $this->pluck('minReadTimestamp', $options), - 'maxStaleness' => $this->pluck('maxStaleness', $options), - 'readTimestamp' => $this->pluck('readTimestamp', $options), - 'exactStaleness' => $this->pluck('exactStaleness', $options), - ]) - ]; - - if (empty($options['transactionOptions']['readOnly'])) { - $options['transactionOptions']['readOnly']['strong'] = true; + // These are only available in single-use transactions. + if (isset($options['maxStaleness']) || isset($options['minReadTimestamp'])) { + throw new \BadMethodCallException( + 'maxStaleness and minReadTimestamp are only available in single-use transactions.' + ); } - $timestampFields = [ - 'minReadTimestamp', - 'readTimestamp' - ]; - - foreach ($timestampFields as $tsf) { - if (isset($options['transactionOptions']['readOnly'][$tsf])) { - $field = $options['transactionOptions']['readOnly'][$tsf]; - if (!($field instanceof Timestamp)) { - throw new \InvalidArgumentException(sprintf( - 'Read Only Transaction Configuration Field %s must be an instance of Timestamp', - $tsf - )); - } - - $options['transactionOptions']['readOnly'][$tsf] = $field->formatAsString(); - } - } + $transactionOptions = $this->configureSnapshotOptions($options); $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); - return $this->operation->snapshot($session, $options); + return $this->operation->snapshot($session, $transactionOptions); } /** @@ -497,7 +458,7 @@ public function runTransaction(callable $operation, array $options = []) } /** - * Create and return a new Transaction. + * Create and return a new read/write Transaction. * * When manually using a Transaction, it is advised that retry logic be * implemented to reapply all operations when an instance of @@ -591,6 +552,7 @@ public function insertBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } @@ -661,6 +623,7 @@ public function updateBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } @@ -734,6 +697,7 @@ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } @@ -807,6 +771,7 @@ public function replaceBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } @@ -839,6 +804,7 @@ public function delete($table, KeySet $keySet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } @@ -861,20 +827,58 @@ public function delete($table, KeySet $keySet, array $options = []) * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ExecuteSqlRequest ExecuteSqlRequest * @codingStandardsIgnoreEnd * + * @codingStandardsIgnoreStart * @param string $sql The query string to execute. * @param array $options [optional] { - * Configuration options. + * Configuration Options. + * + * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions) + * for detailed description of available transaction options. + * + * Please note that only one of `$strong`, `$minReadTimestamp`, + * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in + * a request. * * @type array $parameters A key/value array of Query Parameters, where * the key is represented in the query string prefixed by a `@` * symbol. + * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected + * read timestamp is included in the Transaction message that + * describes the transaction. + * @type bool $strong Read at a timestamp where all previously committed + * transactions are visible. + * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the + * given timestamp. Only available in single-use transactions. + * @type Duration $maxStaleness Read data at a timestamp >= NOW - the + * given timestamp. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type Duration $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. + * @type bool $begin If true, will begin a new transaction. If a + * read/write transaction is desired, set the value of + * $transactionType. If a transaction or snapshot is created, it + * will be returned as `$result->transaction()` or + * `$result->snapshot()`. **Defaults to** `false`. + * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ` + * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is + * chosen, any snapshot options will be disregarded. If `$begin` + * is false, this option will be ignored. **Defaults to** + * `SessionPoolInterface::CONTEXT_READ`. * } + * @codingStandardsIgnoreEnd * @return Result */ public function execute($sql, array $options = []) { $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + list($type, $context, $transaction) = $this->transactionSelector($options); + $options['transaction'] = [ + $type => $transaction + ]; + + $options['transactionContext'] = $context; return $this->operation->execute($session, $sql, $options); } @@ -897,22 +901,61 @@ public function execute($sql, array $options = []) * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest ReadRequest * + * @codingStandardsIgnoreStart * @param string $table The table name. * @param KeySet $keySet The KeySet to select rows. * @param array $columns A list of column names to return. * @param array $options [optional] { * Configuration Options. * + * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions) + * for detailed description of available transaction options. + * + * Please note that only one of `$strong`, `$minReadTimestamp`, + * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in + * a request. + * * @type string $index The name of an index on the table. * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. + * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected + * read timestamp is included in the Transaction message that + * describes the transaction. + * @type bool $strong Read at a timestamp where all previously committed + * transactions are visible. + * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the + * given timestamp. Only available in single-use transactions. + * @type Duration $maxStaleness Read data at a timestamp >= NOW - the + * given timestamp. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type Duration $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. + * @type bool $begin If true, will begin a new transaction. If a + * read/write transaction is desired, set the value of + * $transactionType. If a transaction or snapshot is created, it + * will be returned as `$result->transaction()` or + * `$result->snapshot()`. **Defaults to** `false`. + * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ` + * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is + * chosen, any snapshot options will be disregarded. If `$begin` + * is false, this option will be ignored. **Defaults to** + * `SessionPoolInterface::CONTEXT_READ`. * } + * @codingStandardsIgnoreEnd * @return Result */ public function read($table, KeySet $keySet, array $columns, array $options = []) { $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + list($type, $context, $transaction) = $this->transactionSelector($options); + $options['transaction'] = [ + $type => $transaction + ]; + + $options['transactionContext'] = $context; + return $this->operation->read($session, $table, $keySet, $columns, $options); } diff --git a/src/Spanner/Duration.php b/src/Spanner/Duration.php new file mode 100644 index 000000000000..d7a4b2553f78 --- /dev/null +++ b/src/Spanner/Duration.php @@ -0,0 +1,120 @@ +spanner(); + * + * $seconds = 100; + * $nanoSeconds = 000001; + * $duration = $spanner->duration($seconds, $nanoSeconds); + * ``` + * + * ``` + * // Date objects can be cast to strings for easy display. + * echo (string) $date; + * ``` + */ +class Duration implements ValueInterface +{ + /** + * @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 'DURATION'; + } + + /** + * Format the value as a string. + * + * Example: + * ``` + * echo $date->formatAsString(); + * ``` + * + * @return string + */ + public function formatAsString() + { + return json_encode($this->get()); + } + + /** + * Format the value as a string. + * + * @return string + * @access private + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index ffd1c618bc19..0d678d674af9 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -78,7 +78,7 @@ public function __construct(ConnectionInterface $connection, $returnInt64AsObjec */ public function mutation($operation, $table, $mutation) { - $mutation = $this->arrayFilterPreserveBool($mutation); + $mutation = $this->arrayFilterRemoveNull($mutation); return [ $operation => [ @@ -115,17 +115,20 @@ public function deleteMutation($table, KeySet $keySet) * @param array $options [optional] { * Configuration options. * - * @type int $maxRetries + * @type string $transactionId * } * @return Timestamp The commit Timestamp. */ - public function commit(Session $session, Transaction $transaction, array $options = []) + public function commit(Session $session, array $mutations, array $options = []) { - return $this->connection->commit([ - 'transactionId' => $transaction->id(), - 'mutations' => $transaction->mutations(), + $options += [ + 'transactionId' => null + ]; + + $res = $this->connection->commit($this->arrayFilterRemoveNull([ + 'mutations' => $mutations, 'session' => $session->name() - ] + $options); + ]) + $options); return $this->mapper->createTimestampWithNanos($res['commitTimestamp']); } @@ -160,17 +163,20 @@ public function execute(Session $session, $sql, array $options = []) { $options += [ 'parameters' => [], - 'transactionId' => null, + 'transactionContext' => null ]; $parameters = $this->pluck('parameters', $options); $options += $this->mapper->formatParamsForExecuteSql($parameters); + + $context = $this->pluck('transactionContext', $options); + $res = $this->connection->executeSql([ 'sql' => $sql, 'session' => $session->name() ] + $options); - return $this->createResult($res); + return $this->createResult($session, $res, $context); } /** @@ -195,18 +201,18 @@ public function read(Session $session, $table, KeySet $keySet, array $columns, a 'index' => null, 'limit' => null, 'offset' => null, - 'transactionId' => null, + 'transactionContext' => null ]; + $context = $this->pluck('transactionContext', $options); $res = $this->connection->read([ 'table' => $table, 'session' => $session->name(), - 'transaction' => $options['transactionId'], 'columns' => $columns, 'keySet' => $this->flattenKeySet($keySet) ] + $options); - return $this->createResult($res); + return $this->createResult($session, $res, $context); } /** @@ -224,7 +230,7 @@ public function read(Session $session, $table, KeySet $keySet, array $columns, a public function transaction(Session $session, array $options = []) { $res = $this->beginTransaction($session, $options); - return new Transaction($this, $session, $res['id']); + return $this->createTransaction($session, $res); } /** @@ -240,12 +246,7 @@ public function snapshot(Session $session, array $options = []) { $res = $this->beginTransaction($session, $options); - $timestamp = null; - if (isset($res['readTimestamp'])) { - $timestamp = $this->mapper->createTimestampWithNanos($res['readTimestamp']); - } - - return new Snapshot($this, $session, $res['id'], $timestamp); + return $this->createSnapshot($session, $res); } /** @@ -268,15 +269,46 @@ private function beginTransaction(Session $session, array $options = []) ]); } + /** + * Create a Transaction instance from a response object. + * + * @param Session $session The session the transaction belongs to. + * @param array $res The transaction response. + * @return Transaction + */ + private function createTransaction(Session $session, array $res) + { + return new Transaction($this, $session, $res['id']); + } + + /** + * Create a Snapshot instance from a response object. + * + * @param Session $session The session the snapshot belongs to. + * @param array $res The snapshot response. + * @return Snapshot + */ + private function createSnapshot(Session $session, array $res) + { + $timestamp = null; + if (isset($res['readTimestamp'])) { + $timestamp = $this->mapper->createTimestampWithNanos($res['readTimestamp']); + } + + return new Snapshot($this, $session, $res['id'], $timestamp); + } + /** * Transform a service read or executeSql response to a friendly result. * * @codingStandardsIgnoreStart + * @param Session $session The current session. * @param array $res [ResultSet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet) - * @codingStandardsIgnoreEnd + * @param string $transactionContext * @return Result + * @codingStandardsIgnoreEnd */ - private function createResult(array $res) + private function createResult(Session $session, array $res, $transactionContext) { $columns = isset($res['metadata']['rowType']['fields']) ? $res['metadata']['rowType']['fields'] @@ -289,7 +321,16 @@ private function createResult(array $res) } } - return new Result($res, $rows); + $options = []; + if (isset($res['metadata']['transaction']['id'])) { + if ($transactionContext === SessionPoolInterface::CONTEXT_READ) { + $options['snapshot'] = $this->createSnapshot($session, $res['metadata']['transaction']); + } else { + $options['transaction'] = $this->createTransaction($session, $res['metadata']['transaction']); + } + } + + return new Result($res, $rows, $options); } /** @@ -323,7 +364,7 @@ private function flattenKeySet(KeySet $keySet) $keys['keys'] = $this->mapper->encodeValuesAsSimpleType($keys['keys']); } - return $this->arrayFilterPreserveBool($keys); + return $this->arrayFilterRemoveNull($keys); } /** @@ -336,7 +377,6 @@ public function __debugInfo() { return [ 'connection' => get_class($this->connection), - 'sessionPool' => $this->sessionPool ]; } } diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index fc1d1a5160d5..6bdb6716a429 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -45,14 +45,21 @@ class Result implements \IteratorAggregate */ private $rows; + /** + * @var array + */ + private $options; + /** * @param array $result The query or read result. * @param array $rows The rows, formatted and decoded. + * @param array $options Additional result options and info. */ - public function __construct(array $result, array $rows) + public function __construct(array $result, array $rows, array $options = []) { $this->result = $result; $this->rows = $rows; + $this->options = $options; } /** @@ -152,6 +159,20 @@ public function info() return $this->result; } + public function transaction() + { + return (isset($this->options['transaction'])) + ? $this->options['transaction'] + : null; + } + + public function snapshot() + { + return (isset($this->options['snapshot'])) + ? $this->options['snapshot'] + : null; + } + /** * @access private */ diff --git a/src/Spanner/Session/Session.php b/src/Spanner/Session/Session.php index b0b811b3765a..f2f86584a7cf 100644 --- a/src/Spanner/Session/Session.php +++ b/src/Spanner/Session/Session.php @@ -156,4 +156,15 @@ public function name() $this->name ); } + + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'instance' => $this->instance, + 'database' => $this->database, + 'name' => $this->name, + ]; + } } diff --git a/src/Spanner/Snapshot.php b/src/Spanner/Snapshot.php index 3cf458d87925..87e411c486ae 100644 --- a/src/Spanner/Snapshot.php +++ b/src/Spanner/Snapshot.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Spanner; use Google\Cloud\Spanner\Session\Session; +use Google\Cloud\Spanner\Session\SessionPoolInterface; /** * Read-only snapshot Transaction. @@ -58,6 +59,7 @@ public function __construct( $this->session = $session; $this->transactionId = $transactionId; $this->readTimestamp = $readTimestamp; + $this->context = SessionPoolInterface::CONTEXT_READWRITE; } /** diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 55c169534676..5f74940e4f66 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -419,6 +419,11 @@ public function int64($value) return new Int64($value); } + public function duration($seconds, $nanos = 0) + { + return new Duration($seconds, $nanos); + } + /** * Get the session client * diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 9060e9d1b28d..96c1b73a17da 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -94,6 +94,7 @@ public function __construct( $this->operation = $operation; $this->session = $session; $this->transactionId = $transactionId; + $this->context = SessionPoolInterface::CONTEXT_READWRITE; } /** @@ -333,7 +334,7 @@ public function rollback(array $options = []) return $this->operation->rollback($this->session, $this, $options); } - public function commit() + public function commit(array $options = []) { if ($this->state !== self::STATE_ACTIVE) { throw new \RuntimeException('The transaction cannot be committed because it is not active'); @@ -341,7 +342,8 @@ public function commit() $this->state = self::STATE_COMMITTED; - return $this->operation->commit($this->session, $this); + $options['transactionId'] = $this->transactionId; + return $this->operation->commit($this->session, $this->mutations, $options); } /** diff --git a/src/Spanner/TransactionConfigurationTrait.php b/src/Spanner/TransactionConfigurationTrait.php new file mode 100644 index 000000000000..6e19faaa9283 --- /dev/null +++ b/src/Spanner/TransactionConfigurationTrait.php @@ -0,0 +1,157 @@ + false, + 'transactionType' => SessionPoolInterface::CONTEXT_READ, + 'transactionId' => null + ]; + + $type = null; + + $context = $this->pluck('transactionType', $options); + $id = $this->pluck('transactionId', $options); + if (!is_null($id)) { + $type = 'id'; + $transactionOptions = $id; + } elseif ($context === SessionPoolInterface::CONTEXT_READ) { + $transactionOptions = $this->configureSnapshotOptions($options); + } elseif ($context === SessionPoolInterface::CONTEXT_READWRITE) { + $transactionOptions = $this->configureTransactionOptions(); + } else { + throw new \BadMethodCallException(sprintf( + 'Invalid transaction context %s', + $context + )); + } + + $this->pluck('begin', $options); + if (is_null($type)) { + $type = ($begin) ? 'begin' : 'singleUse'; + } + + return [$type, $context, $transactionOptions]; + } + + private function configureTransactionOptions() + { + return [ + 'readWrite' => [] + ]; + } + + /** + * Create a Read Only single use transaction. + * + * @param array $options Configuration Options. + * @return array + */ + private function configureSnapshotOptions(array &$options) + { + $options += [ + 'returnReadTimestamp' => null, + 'strong' => null, + 'readTimestamp' => null, + 'exactStaleness' => null, + 'minReadTimestamp' => null, + 'maxStaleness' => null, + ]; + + if (!is_null($options['exactStaleness']) && !($options['exactStaleness'] instanceof Duration)) { + throw new \BadMethodCallException('$options.exactStaleness must be an intance of Duration'); + } + + if (!is_null($options['maxStaleness']) && !($options['maxStaleness'] instanceof Duration)) { + throw new \BadMethodCallException('$options.maxStaleness must be an intance of Duration'); + } + + $transactionOptions = [ + 'readOnly' => $this->arrayFilterRemoveNull([ + 'returnReadTimestamp' => $this->pluck('returnReadTimestamp', $options), + 'strong' => $this->pluck('strong', $options), + 'minReadTimestamp' => $this->pluck('minReadTimestamp', $options), + 'maxStaleness' => $this->pluck('maxStaleness', $options), + 'readTimestamp' => $this->pluck('readTimestamp', $options), + 'exactStaleness' => $this->pluck('exactStaleness', $options), + ]) + ]; + + if (empty($transactionOptions['readOnly'])) { + $transactionOptions['readOnly']['strong'] = true; + } + + $timestampFields = [ + 'minReadTimestamp', + 'readTimestamp' + ]; + + $durationFields = [ + 'exactStaleness', + 'maxStaleness' + ]; + + foreach ($timestampFields as $tsf) { + if (isset($transactionOptions['readOnly'][$tsf])) { + $field = $transactionOptions['readOnly'][$tsf]; + if (!($field instanceof Timestamp)) { + throw new \InvalidArgumentException(sprintf( + 'Read Only Transaction Configuration Field %s must be an instance of Timestamp', + $tsf + )); + } + + $transactionOptions['readOnly'][$tsf] = $field->formatAsString(); + } + } + + foreach ($durationFields as $df) { + if (isset($transactionOptions['readOnly'][$df])) { + $field = $transactionOptions['readOnly'][$df]; + if (!($field instanceof Duration)) { + throw new \InvalidArgumentException(sprintf( + 'Read Only Transaction Configuration Field %s must be an instance of Duration', + $df + )); + } + + $transactionOptions['readOnly'][$df] = $field->get(); + } + } + + return $transactionOptions; + } +} diff --git a/src/Spanner/TransactionReadTrait.php b/src/Spanner/TransactionReadTrait.php index 354f2dd1d903..5e8637d742ab 100644 --- a/src/Spanner/TransactionReadTrait.php +++ b/src/Spanner/TransactionReadTrait.php @@ -22,6 +22,8 @@ */ trait TransactionReadTrait { + use TransactionConfigurationTrait; + /** * @var Operation */ @@ -37,6 +39,11 @@ trait TransactionReadTrait */ private $transactionId; + /** + * @var string + */ + private $context; + /** * Run a query. * @@ -63,9 +70,16 @@ trait TransactionReadTrait */ public function execute($sql, array $options = []) { - return $this->operation->execute($this->session, $sql, [ - 'transactionId' => $this->transactionId - ] + $options); + $options['transactionType'] = $this->context; + $options['transactionId'] = $this->transactionId; + + list($type, $context, $transaction) = $this->transactionSelector($options); + $options['transaction'] = [ + $type => $transaction + ]; + + $options['transactionContext'] = $context; + return $this->operation->execute($this->session, $sql, $options); } /** @@ -99,9 +113,16 @@ public function execute($sql, array $options = []) */ public function read($table, KeySet $keySet, array $columns, array $options = []) { - return $this->operation->read($this->session, $table, $keySet, $columns, [ - 'transactionId' => $this->transactionId - ] + $options); + $options['transactionType'] = $this->context; + $options['transactionId'] = $this->transactionId; + + list($type, $context, $transaction) = $this->transactionSelector($options); + $options['transaction'] = [ + $type => $transaction + ]; + + $options['transactionContext'] = $context; + return $this->operation->read($this->session, $table, $keySet, $columns, $options); } /** From e8b991ae5b62ca943ea8dc6dee68bd8bc6a5065a Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Thu, 26 Jan 2017 16:23:45 -0500 Subject: [PATCH 047/107] Fix tests --- src/Spanner/Connection/Grpc.php | 56 +++++++------ src/Spanner/Operation.php | 6 +- src/Spanner/Transaction.php | 2 +- src/Spanner/TransactionConfigurationTrait.php | 2 +- tests/unit/Spanner/DatabaseTest.php | 81 +------------------ tests/unit/Spanner/OperationTest.php | 80 ++---------------- tests/unit/Spanner/TransactionTest.php | 43 ++-------- 7 files changed, 52 insertions(+), 218 deletions(-) diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 5ffc842f9362..212e78ffc8a3 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -171,18 +171,6 @@ public function updateInstance(array $args = []) ]); } - private function instanceObject(array &$args, $required = false) - { - return (new Instance())->deserialize(array_filter([ - 'name' => $this->pluck('name', $args, $required), - 'config' => $this->pluck('config', $args, $required), - 'displayName' => $this->pluck('displayName', $args, $required), - 'nodeCount' => $this->pluck('nodeCount', $args, $required), - 'state' => $this->pluck('state', $args, $required), - 'labels' => $this->formatLabelsForApi($this->pluck('labels', $args, $required)) - ]), $this->codec); - } - /** * @param array $args [optional] */ @@ -401,18 +389,6 @@ public function read(array $args = []) ]); } - private function createTransactionSelector(array $args) - { - $selector = new TransactionSelector; - if (isset($args['transaction'])) { - $selector = $selector->deserialize($this->pluck('transaction', $args), $this->codec); - } elseif (isset($args['transactionId'])) { - $selector = $selector->deserialize(['id' => $this->pluck('transactionId', $args)], $this->codec); - } - - return $selector; - } - /** * @param array $args [optional] */ @@ -540,4 +516,36 @@ private function formatKeySet(array $keySet) return $keySet; } + + /** + * @param array $args + * @return array + */ + private function createTransactionSelector(array &$args) + { + $selector = new TransactionSelector; + if (isset($args['transaction'])) { + $selector = $selector->deserialize($this->pluck('transaction', $args), $this->codec); + } elseif (isset($args['transactionId'])) { + $selector = $selector->deserialize(['id' => $this->pluck('transactionId', $args)], $this->codec); + } + + return $selector; + } + + /** + * @param array $args + * @param bool $isRequired + */ + private function instanceObject(array &$args, $required = false) + { + return (new Instance())->deserialize(array_filter([ + 'name' => $this->pluck('name', $args, $required), + 'config' => $this->pluck('config', $args, $required), + 'displayName' => $this->pluck('displayName', $args, $required), + 'nodeCount' => $this->pluck('nodeCount', $args, $required), + 'state' => $this->pluck('state', $args, $required), + 'labels' => $this->formatLabelsForApi($this->pluck('labels', $args, $required)) + ]), $this->codec); + } } diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 0d678d674af9..b49348ccfb2b 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -139,14 +139,14 @@ public function commit(Session $session, array $mutations, array $options = []) * @param Session $session The session to use for the rollback. * Note that the session MUST be the same one in which the * transaction was created. - * @param Transaction $transaction The transaction to roll back. + * @param string $transactionId The transaction to roll back. * @param array $options [optional] Configuration Options. * @return void */ - public function rollback(Session $session, Transaction $transaction, array $options = []) + public function rollback(Session $session, $transactionId, array $options = []) { return $this->connection->rollback([ - 'transactionId' => $transaction->id(), + 'transactionId' => $transactionId, 'session' => $session->name() ] + $options); } diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 96c1b73a17da..aa275700f7ca 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -331,7 +331,7 @@ public function rollback(array $options = []) $this->state = self::STATE_ROLLED_BACK; - return $this->operation->rollback($this->session, $this, $options); + return $this->operation->rollback($this->session, $this->transactionId, $options); } public function commit(array $options = []) diff --git a/src/Spanner/TransactionConfigurationTrait.php b/src/Spanner/TransactionConfigurationTrait.php index 6e19faaa9283..42c8bbf1742c 100644 --- a/src/Spanner/TransactionConfigurationTrait.php +++ b/src/Spanner/TransactionConfigurationTrait.php @@ -59,7 +59,7 @@ private function transactionSelector(array &$options) )); } - $this->pluck('begin', $options); + $begin = $this->pluck('begin', $options); if (is_null($type)) { $type = ($begin) ? 'begin' : 'singleUse'; } diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php index f26d9c4a9314..2061270d9e54 100644 --- a/tests/unit/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/DatabaseTest.php @@ -19,6 +19,7 @@ use Google\Cloud\Spanner\Connection\ConnectionInterface; 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; @@ -77,81 +78,6 @@ public function setUp() $this->database = \Google\Cloud\Dev\stub(Database::class, $args, $props); } - public function testReadOnlyTransaction() - { - $this->connection->beginTransaction(Argument::that(function($arg) { - if ($arg['transactionOptions']['readOnly']['strong'] !== TRUE) return false; - - return true; - })) - ->shouldBeCalled() - ->willReturn([ - 'id' => self::TRANSACTION - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); - - $t = $this->database->readOnlyTransaction(); - $this->assertInstanceOf(Transaction::class, $t); - } - - public function testReadOnlyTransactionOptions() - { - $options = [ - 'returnReadTimestamp' => true, - 'strong' => false, - 'minReadTimestamp' => new Timestamp(new \DateTime), - 'maxStaleness' => 1337, - 'readTimestamp' => new Timestamp(new \DateTime), - 'exactStaleness' => 7331 - ]; - - $this->connection->beginTransaction(Argument::that(function($arg) use ($options) { - if ($arg['transactionOptions']['readOnly']['returnReadTimestamp'] !== $options['returnReadTimestamp']) return false; - if ($arg['transactionOptions']['readOnly']['strong'] !== $options['strong']) return false; - if ($arg['transactionOptions']['readOnly']['minReadTimestamp'] !== $options['minReadTimestamp']->formatAsString()) return false; - if ($arg['transactionOptions']['readOnly']['maxStaleness'] !== $options['maxStaleness']) return false; - if ($arg['transactionOptions']['readOnly']['readTimestamp'] !== $options['readTimestamp']->formatAsString()) return false; - if ($arg['transactionOptions']['readOnly']['exactStaleness'] !== $options['exactStaleness']) return false; - - return true; - })) - ->shouldBeCalled() - ->willReturn([ - 'id' => self::TRANSACTION - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); - - $this->database->readOnlyTransaction($options); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testReadOnlyTransactionInvalidConfigType() - { - $t = $this->database->readOnlyTransaction(['minReadTimestamp' => 'foo']); - } - - public function testLockingTransaction() - { - $this->connection->beginTransaction(Argument::that(function($arg) { - if (!isset($arg['transactionOptions']['readWrite'])) return false; - - return true; - })) - ->shouldBeCalled() - ->willReturn([ - 'id' => self::TRANSACTION - ]); - - $this->database->___setProperty('connection', $this->connection->reveal()); - - $t = $this->database->lockingTransaction(); - $this->assertInstanceOf(Transaction::class, $t); - } - public function testInsert() { $table = 'foo'; @@ -374,7 +300,8 @@ public function testRead() $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { if ($arg['table'] !== $table) return false; - if ($arg['foo'] !== $opts['foo']) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['ID']) return false; return true; }))->shouldBeCalled()->willReturn([ @@ -399,7 +326,7 @@ public function testRead() $this->refreshOperation(); - $res = $this->database->read($table, $opts); + $res = $this->database->read($table, new KeySet(['all' => true]), ['ID']); $this->assertInstanceOf(Result::class, $res); $this->assertEquals(10, $res->rows()[0]['ID']); } diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php index 3f3ea0e29800..2461b3df1643 100644 --- a/tests/unit/Spanner/OperationTest.php +++ b/tests/unit/Spanner/OperationTest.php @@ -100,14 +100,16 @@ public function testCommit() $this->connection->commit(Argument::that(function ($arg) use ($mutations) { if ($arg['mutations'] !== $mutations) return false; - if ($arg['singleUseTransaction']['readWrite'] !== []) return false; + if ($arg['transactionId'] !== 'foo') return false; return true; }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); $this->operation->___setProperty('connection', $this->connection->reveal()); - $res = $this->operation->commit($this->session, $mutations); + $res = $this->operation->commit($this->session, $mutations, [ + 'transactionId' => 'foo' + ]); $this->assertInstanceOf(Timestamp::class, $res); } @@ -181,88 +183,18 @@ public function testRead() if ($arg['table'] !== 'Posts') return false; if ($arg['session'] !== self::SESSION) return false; if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; return true; }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); $this->operation->___setProperty('connection', $this->connection->reveal()); - $res = $this->operation->read($this->session, 'Posts'); - $this->assertInstanceOf(Result::class, $res); - $this->assertEquals(10, $res->rows()[0]['ID']); - } - - public function testReadWithKeySet() - { - $keys = ['foo','bar']; - - $this->connection->read(Argument::that(function ($arg) use ($keys) { - if ($arg['table'] !== 'Posts') return false; - if ($arg['session'] !== self::SESSION) return false; - if ($arg['keySet']['all'] === true) return false; - if ($arg['keySet']['keys'] !== $keys) return false; - - return true; - }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); - - $this->operation->___setProperty('connection', $this->connection->reveal()); - - $res = $this->operation->read($this->session, 'Posts', [ - 'keySet' => new KeySet(['keys' => $keys]) - ]); + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo']); $this->assertInstanceOf(Result::class, $res); $this->assertEquals(10, $res->rows()[0]['ID']); } - /** - * @expectedException InvalidArgumentException - */ - public function testReadWithInvalidKeySet() - { - $this->operation->read($this->session, 'Posts', [ - 'keySet' => 'foo' - ]); - } - - public function testTransaction() - { - $this->connection->beginTransaction(Argument::that(function ($arg) { - if ($arg['session'] !== self::SESSION) return false; - - return true; - }))->shouldBeCalled()->willReturn([ - 'id' => self::TRANSACTION - ]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); - - $res = $this->operation->transaction($this->session, SessionPoolInterface::CONTEXT_READWRITE); - - $this->assertInstanceOf(Transaction::class, $res); - $this->assertEquals(self::TRANSACTION, $res->id()); - $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $res->context()); - $this->assertNull($res->readTimestamp()); - } - - public function testTransactionWithTimestamp() - { - $this->connection->beginTransaction(Argument::that(function ($arg) { - if ($arg['session'] !== self::SESSION) return false; - - return true; - }))->shouldBeCalled()->willReturn([ - 'id' => self::TRANSACTION, - 'readTimestamp' => self::TIMESTAMP - ]); - - $this->operation->___setProperty('connection', $this->connection->reveal()); - - $res = $this->operation->transaction($this->session, SessionPoolInterface::CONTEXT_READWRITE); - - $this->assertInstanceOf(Transaction::class, $res); - $this->assertInstanceOf(Timestamp::class, $res->readTimestamp()); - } - private function executeAndReadResponse() { return [ diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php index 400704f707ca..e9167bf018ec 100644 --- a/tests/unit/Spanner/TransactionTest.php +++ b/tests/unit/Spanner/TransactionTest.php @@ -63,7 +63,6 @@ public function setUp() $args = [ $this->operation, $this->session, - SessionPoolInterface::CONTEXT_READWRITE, self::TRANSACTION, ]; @@ -177,7 +176,7 @@ public function testExecute() $sql = 'SELECT * FROM Table'; $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { - if ($arg['transactionId'] !== self::TRANSACTION) return false; + if ($arg['transaction']['id'] !== self::TRANSACTION) return false; if ($arg['sql'] !== $sql) return false; return true; @@ -214,9 +213,10 @@ public function testRead() $opts = ['foo' => 'bar']; $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { - if ($arg['transactionId'] !== self::TRANSACTION) return false; + if ($arg['transaction']['id'] !== self::TRANSACTION) return false; if ($arg['table'] !== $table) return false; - if ($arg['foo'] !== $opts['foo']) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['ID']) return false; return true; }))->shouldBeCalled()->willReturn([ @@ -241,7 +241,7 @@ public function testRead() $this->refreshOperation(); - $res = $this->transaction->read($table, $opts); + $res = $this->transaction->read($table, new KeySet(['all' => true]), ['ID']); $this->assertInstanceOf(Result::class, $res); $this->assertEquals(10, $res->rows()[0]['ID']); } @@ -260,24 +260,6 @@ public function testCommit() $this->transaction->commit(); } - /** - * @expectedException RuntimeException - */ - public function testCommitInvalidContext() - { - $this->transaction->___setProperty('context', SessionPoolInterface::CONTEXT_READ); - $this->transaction->commit(); - } - - /** - * @expectedException RuntimeException - */ - public function testEnqueueInvalidContext() - { - $this->transaction->___setProperty('context', SessionPoolInterface::CONTEXT_READ); - $this->transaction->insert('Posts', []); - } - public function testRollback() { $this->connection->rollback(Argument::any()) @@ -288,26 +270,11 @@ public function testRollback() $this->transaction->rollback(); } - public function testReadTimestamp() - { - $this->transaction->___setProperty('context', SessionPoolInterface::CONTEXT_READ); - $this->transaction->___setProperty('readTimestamp', new Timestamp(new \DateTimeImmutable(self::TIMESTAMP))); - - $ts = $this->transaction->readTimestamp(); - - $this->assertInstanceOf(Timestamp::class, $ts); - } - public function testId() { $this->assertEquals(self::TRANSACTION, $this->transaction->id()); } - public function testContext() - { - $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $this->transaction->context()); - } - // ******* // Helpers From afa78d6e33710665e3b46cd39d64f90a00760cd9 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Thu, 26 Jan 2017 17:09:05 -0500 Subject: [PATCH 048/107] Add unit test coverage --- src/Spanner/Database.php | 3 +- src/Spanner/Duration.php | 4 +- tests/unit/Spanner/DatabaseTest.php | 126 +++++++++++++++++++++++++ tests/unit/Spanner/DurationTest.php | 65 +++++++++++++ tests/unit/Spanner/ResultTest.php | 38 ++++++++ tests/unit/Spanner/SnapshotTest.php | 48 ++++++++++ tests/unit/Spanner/TransactionTest.php | 35 ++++++- tests/unit/Spanner/ValueMapperTest.php | 10 ++ 8 files changed, 326 insertions(+), 3 deletions(-) create mode 100644 tests/unit/Spanner/DurationTest.php create mode 100644 tests/unit/Spanner/SnapshotTest.php diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index d4f2456382d9..de2cf2e02df1 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -440,7 +440,8 @@ public function runTransaction(callable $operation, array $options = []) throw $e; } - return $e->getRetryDelay(); + $delay = $e->getRetryDelay(); + time_nanosleep($delay['seconds'], $delay['nanos']); }; $commitFn = function($operation, $session, $options) use ($startTransactionFn) { diff --git a/src/Spanner/Duration.php b/src/Spanner/Duration.php index d7a4b2553f78..6b489b90cc96 100644 --- a/src/Spanner/Duration.php +++ b/src/Spanner/Duration.php @@ -39,6 +39,8 @@ */ class Duration implements ValueInterface { + const TYPE = 'DURATION'; + /** * @var int */ @@ -89,7 +91,7 @@ public function get() */ public function type() { - return 'DURATION'; + return self::TYPE; } /** diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php index 2061270d9e54..567ab1dde2c3 100644 --- a/tests/unit/Spanner/DatabaseTest.php +++ b/tests/unit/Spanner/DatabaseTest.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Tests\Spanner; +use Google\Cloud\Exception\AbortedException; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Duration; @@ -26,6 +27,7 @@ use Google\Cloud\Spanner\Result; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\Snapshot; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; use Google\Cloud\Spanner\ValueMapper; @@ -78,6 +80,130 @@ public function setUp() $this->database = \Google\Cloud\Dev\stub(Database::class, $args, $props); } + public function testSnapshot() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $res = $this->database->snapshot(); + $this->assertInstanceOf(Snapshot::class, $res); + } + + /** + * @expectedException BadMethodCallException + */ + public function testSnapshotMinReadTimestamp() + { + $this->database->snapshot(['minReadTimestamp' => 'foo']); + } + + /** + * @expectedException BadMethodCallException + */ + public function testSnapshotMaxStaleness() + { + $this->database->snapshot(['maxStaleness' => 'foo']); + } + + public function testRunTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $hasTransaction = false; + + $this->database->runTransaction(function (Transaction $t) use (&$hasTransaction) { + $hasTransaction = true; + }); + + $this->assertTrue($hasTransaction); + } + + public function testRunTransactionRetry() + { + $abort = new AbortedException('foo', 409, null, [ + [ + 'retryDelay' => [ + 'seconds' => 1, + 'nanos' => 0 + ] + ] + ]); + + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalledTimes(3) + ->willReturn(['id' => self::TRANSACTION]); + + $it = 0; + $this->connection->commit(Argument::any()) + ->shouldBeCalledTimes(3) + ->will(function() use (&$it, $abort) { + $it++; + if ($it <= 2) { + throw $abort; + } + + return ['commitTimestamp' => TransactionTest::TIMESTAMP]; + }); + + $this->refreshOperation(); + + $this->database->runTransaction(function($t){$t->commit();}); + } + + /** + * @expectedException Google\Cloud\Exception\AbortedException + */ + public function testRunTransactionAborted() + { + $abort = new AbortedException('foo', 409, null, [ + [ + 'retryDelay' => [ + 'seconds' => 1, + 'nanos' => 0 + ] + ] + ]); + + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $it = 0; + $this->connection->commit(Argument::any()) + ->shouldBeCalled() + ->will(function() use (&$it, $abort) { + $it++; + if ($it <= 8) { + throw $abort; + } + + return ['commitTimestamp' => TransactionTest::TIMESTAMP]; + }); + + $this->refreshOperation(); + + $this->database->runTransaction(function($t){$t->commit();}); + } + + public function testTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $t = $this->database->transaction(); + $this->assertInstanceOf(Transaction::class, $t); + } + public function testInsert() { $table = 'foo'; diff --git a/tests/unit/Spanner/DurationTest.php b/tests/unit/Spanner/DurationTest.php new file mode 100644 index 000000000000..37d2575e42f7 --- /dev/null +++ b/tests/unit/Spanner/DurationTest.php @@ -0,0 +1,65 @@ +duration = new Duration(self::SECONDS, self::NANOS); + } + + public function testGet() + { + $this->assertEquals([ + 'seconds' => self::SECONDS, + 'nanos' => self::NANOS + ], $this->duration->get()); + } + + public function testType() + { + $this->assertEquals(Duration::TYPE, $this->duration->type()); + } + + public function testFormatAsString() + { + $this->assertEquals( + json_encode($this->duration->get()), + $this->duration->formatAsString() + ); + } + + public function testTostring() + { + $this->assertEquals( + json_encode($this->duration->get()), + (string)$this->duration + ); + } +} diff --git a/tests/unit/Spanner/ResultTest.php b/tests/unit/Spanner/ResultTest.php index e5fa12d6c38a..98fa6d8ef6b5 100644 --- a/tests/unit/Spanner/ResultTest.php +++ b/tests/unit/Spanner/ResultTest.php @@ -52,6 +52,18 @@ public function testRows() $this->assertEquals($rows, $result->rows()); } + public function testFirstRow() + { + $rows = [ + ['name' => 'John'], + ['name' => 'Dave'] + ]; + + $result = new Result([], $rows); + + $this->assertEquals($rows[0], $result->firstRow()); + } + public function testStats() { $result = new Result(['stats' => 'foo'], []); @@ -65,4 +77,30 @@ public function testInfo() $this->assertEquals($info, $result->info()); } + + public function testTransaction() + { + $result = new Result([], [], [ + 'transaction' => 'foo' + ]); + + $this->assertEquals('foo', $result->transaction()); + + $result = new Result([], []); + + $this->assertNull($result->transaction()); + } + + public function testSnapshot() + { + $result = new Result([], [], [ + 'snapshot' => 'foo' + ]); + + $this->assertEquals('foo', $result->snapshot()); + + $result = new Result([], []); + + $this->assertNull($result->snapshot()); + } } diff --git a/tests/unit/Spanner/SnapshotTest.php b/tests/unit/Spanner/SnapshotTest.php new file mode 100644 index 000000000000..cf54179ce9ed --- /dev/null +++ b/tests/unit/Spanner/SnapshotTest.php @@ -0,0 +1,48 @@ +timestamp = new Timestamp(new \DateTime); + $this->snapshot = new Snapshot( + $this->prophesize(Operation::class)->reveal(), + $this->prophesize(Session::class)->reveal(), + 'foo', + $this->timestamp + ); + } + + public function testReadTimestamp() + { + $this->assertEquals($this->timestamp, $this->snapshot->readTimestamp()); + } +} diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php index e9167bf018ec..bb5d52151b23 100644 --- a/tests/unit/Spanner/TransactionTest.php +++ b/tests/unit/Spanner/TransactionTest.php @@ -67,7 +67,7 @@ public function setUp() ]; $props = [ - 'operation', 'readTimestamp', 'context' + 'operation', 'readTimestamp', 'state' ]; $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props); @@ -260,6 +260,15 @@ public function testCommit() $this->transaction->commit(); } + /** + * @expectedException RuntimeException + */ + public function testCommitInvalidState() + { + $this->transaction->___setProperty('state', 'foo'); + $this->transaction->commit(); + } + public function testRollback() { $this->connection->rollback(Argument::any()) @@ -270,11 +279,35 @@ public function testRollback() $this->transaction->rollback(); } + /** + * @expectedException RuntimeException + */ + public function testRollbackInvalidState() + { + $this->transaction->___setProperty('state', 'foo'); + $this->transaction->rollback(); + } + public function testId() { $this->assertEquals(self::TRANSACTION, $this->transaction->id()); } + public function testState() + { + $this->assertEquals(Transaction::STATE_ACTIVE, $this->transaction->state()); + + $this->transaction->___setProperty('state', Transaction::STATE_COMMITTED); + $this->assertEquals(Transaction::STATE_COMMITTED, $this->transaction->state()); + } + + public function testMutations() + { + $this->assertEmpty($this->transaction->mutations()); + $this->transaction->insert('Posts', []); + $this->assertEquals(1, count($this->transaction->mutations())); + } + // ******* // Helpers diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php index 8d91e010d2a3..11840d40cd0d 100644 --- a/tests/unit/Spanner/ValueMapperTest.php +++ b/tests/unit/Spanner/ValueMapperTest.php @@ -85,6 +85,16 @@ public function testFormatParamsForExecuteSqlArray() $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['array']['arrayElementType']['code']); } + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlArrayInvalidAssoc() + { + $this->mapper->formatParamsForExecuteSql(['array' => [ + 'foo' => 'bar' + ]]); + } + /** * @expectedException InvalidArgumentException */ From b0b96b858626a9af68402845cf8c0f400589851d Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 27 Jan 2017 09:50:08 -0500 Subject: [PATCH 049/107] Additional unit test coverage --- src/ArrayTrait.php | 4 +- src/Spanner/Database.php | 15 +- src/Spanner/Snapshot.php | 3 + src/Spanner/TransactionConfigurationTrait.php | 17 +- src/Spanner/TransactionReadTrait.php | 34 +--- tests/unit/ArrayTraitTest.php | 20 ++ tests/unit/Spanner/OperationTest.php | 91 ++++++++- tests/unit/Spanner/SpannerClientTest.php | 7 + .../TransactionConfigurationTraitTest.php | 185 ++++++++++++++++++ 9 files changed, 322 insertions(+), 54 deletions(-) create mode 100644 tests/unit/Spanner/TransactionConfigurationTraitTest.php diff --git a/src/ArrayTrait.php b/src/ArrayTrait.php index 4977863f7dc7..78313b42f67a 100644 --- a/src/ArrayTrait.php +++ b/src/ArrayTrait.php @@ -88,11 +88,11 @@ private function isAssoc(array $arr) private function arrayFilterRemoveNull(array $arr) { return array_filter($arr, function ($element) { - if (is_bool($element)) { + if (!is_null($element)) { return true; } - return $element == true; + return false; }); } } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index de2cf2e02df1..7518bf807f10 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -874,12 +874,10 @@ public function execute($sql, array $options = []) { $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); - list($type, $context, $transaction) = $this->transactionSelector($options); - $options['transaction'] = [ - $type => $transaction - ]; - + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; $options['transactionContext'] = $context; + return $this->operation->execute($session, $sql, $options); } @@ -950,11 +948,8 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] { $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); - list($type, $context, $transaction) = $this->transactionSelector($options); - $options['transaction'] = [ - $type => $transaction - ]; - + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; $options['transactionContext'] = $context; return $this->operation->read($session, $table, $keySet, $columns, $options); diff --git a/src/Spanner/Snapshot.php b/src/Spanner/Snapshot.php index 87e411c486ae..3a7d241e41e1 100644 --- a/src/Spanner/Snapshot.php +++ b/src/Spanner/Snapshot.php @@ -23,6 +23,9 @@ /** * Read-only snapshot Transaction. * + * For full usage details, please refer to + * {@see Google\Cloud\Spanner\Database::snapshot()}. + * * Example: * ``` * use Google\Cloud\ServiceBuilder; diff --git a/src/Spanner/TransactionConfigurationTrait.php b/src/Spanner/TransactionConfigurationTrait.php index 42c8bbf1742c..12ff043713f9 100644 --- a/src/Spanner/TransactionConfigurationTrait.php +++ b/src/Spanner/TransactionConfigurationTrait.php @@ -64,7 +64,10 @@ private function transactionSelector(array &$options) $type = ($begin) ? 'begin' : 'singleUse'; } - return [$type, $context, $transactionOptions]; + return [ + [$type => $transactionOptions], + $context + ]; } private function configureTransactionOptions() @@ -91,14 +94,6 @@ private function configureSnapshotOptions(array &$options) 'maxStaleness' => null, ]; - if (!is_null($options['exactStaleness']) && !($options['exactStaleness'] instanceof Duration)) { - throw new \BadMethodCallException('$options.exactStaleness must be an intance of Duration'); - } - - if (!is_null($options['maxStaleness']) && !($options['maxStaleness'] instanceof Duration)) { - throw new \BadMethodCallException('$options.maxStaleness must be an intance of Duration'); - } - $transactionOptions = [ 'readOnly' => $this->arrayFilterRemoveNull([ 'returnReadTimestamp' => $this->pluck('returnReadTimestamp', $options), @@ -128,7 +123,7 @@ private function configureSnapshotOptions(array &$options) if (isset($transactionOptions['readOnly'][$tsf])) { $field = $transactionOptions['readOnly'][$tsf]; if (!($field instanceof Timestamp)) { - throw new \InvalidArgumentException(sprintf( + throw new \BadMethodCallException(sprintf( 'Read Only Transaction Configuration Field %s must be an instance of Timestamp', $tsf )); @@ -142,7 +137,7 @@ private function configureSnapshotOptions(array &$options) if (isset($transactionOptions['readOnly'][$df])) { $field = $transactionOptions['readOnly'][$df]; if (!($field instanceof Duration)) { - throw new \InvalidArgumentException(sprintf( + throw new \BadMethodCallException(sprintf( 'Read Only Transaction Configuration Field %s must be an instance of Duration', $df )); diff --git a/src/Spanner/TransactionReadTrait.php b/src/Spanner/TransactionReadTrait.php index 5e8637d742ab..44ba7d397e7e 100644 --- a/src/Spanner/TransactionReadTrait.php +++ b/src/Spanner/TransactionReadTrait.php @@ -73,12 +73,10 @@ public function execute($sql, array $options = []) $options['transactionType'] = $this->context; $options['transactionId'] = $this->transactionId; - list($type, $context, $transaction) = $this->transactionSelector($options); - $options['transaction'] = [ - $type => $transaction - ]; - + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; $options['transactionContext'] = $context; + return $this->operation->execute($this->session, $sql, $options); } @@ -116,31 +114,11 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] $options['transactionType'] = $this->context; $options['transactionId'] = $this->transactionId; - list($type, $context, $transaction) = $this->transactionSelector($options); - $options['transaction'] = [ - $type => $transaction - ]; - + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; $options['transactionContext'] = $context; - return $this->operation->read($this->session, $table, $keySet, $columns, $options); - } - /** - * Retrieve the Read Timestamp. - * - * For snapshot read-only transactions, the read timestamp chosen for the - * transaction. - * - * Example: - * ``` - * $timestamp = $transaction->readTimestamp(); - * ``` - * - * @return Timestamp - */ - public function readTimestamp() - { - return $this->readTimestamp; + return $this->operation->read($this->session, $table, $keySet, $columns, $options); } /** diff --git a/tests/unit/ArrayTraitTest.php b/tests/unit/ArrayTraitTest.php index 53dc38667917..538490227988 100644 --- a/tests/unit/ArrayTraitTest.php +++ b/tests/unit/ArrayTraitTest.php @@ -83,6 +83,26 @@ public function testIsAssocFalse() $this->assertFalse($actual); } + + public function testArrayFilterRemoveNull() + { + $input = [ + 'null' => null, + 'false' => false, + 'zero' => 0, + 'float' => 0.0, + 'empty' => '', + 'array' => [], + ]; + + $res = $this->implementation->call('arrayFilterRemoveNull', [$input]); + $this->assertFalse(array_key_exists('null', $res)); + $this->assertTrue(array_key_exists('false', $res)); + $this->assertTrue(array_key_exists('zero', $res)); + $this->assertTrue(array_key_exists('float', $res)); + $this->assertTrue(array_key_exists('empty', $res)); + $this->assertTrue(array_key_exists('array', $res)); + } } class ArrayTraitStub diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php index 2461b3df1643..298e8531632a 100644 --- a/tests/unit/Spanner/OperationTest.php +++ b/tests/unit/Spanner/OperationTest.php @@ -24,6 +24,7 @@ use Google\Cloud\Spanner\Result; use Google\Cloud\Spanner\Session\Session; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\Snapshot; use Google\Cloud\Spanner\Timestamp; use Google\Cloud\Spanner\Transaction; use Google\Cloud\Spanner\ValueMapper; @@ -195,10 +196,94 @@ public function testRead() $this->assertEquals(10, $res->rows()[0]['ID']); } - private function executeAndReadResponse() + public function testReadWithTransaction() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse([ + 'transaction' => ['id' => self::TRANSACTION] + ])); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ + 'transactionContext' => SessionPoolInterface::CONTEXT_READWRITE + ]); + $this->assertInstanceOf(Transaction::class, $res->transaction()); + $this->assertEquals(self::TRANSACTION, $res->transaction()->id()); + } + + public function testReadWithSnapshot() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse([ + 'transaction' => ['id' => self::TRANSACTION] + ])); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ + 'transactionContext' => SessionPoolInterface::CONTEXT_READ + ]); + $this->assertInstanceOf(Snapshot::class, $res->snapshot()); + $this->assertEquals(self::TRANSACTION, $res->snapshot()->id()); + } + + public function testTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $t = $this->operation->transaction($this->session); + $this->assertInstanceOf(Transaction::class, $t); + $this->assertEquals(self::TRANSACTION, $t->id()); + } + + public function testSnapshot() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snap = $this->operation->snapshot($this->session); + $this->assertInstanceOf(Snapshot::class, $snap); + $this->assertEquals(self::TRANSACTION, $snap->id()); + } + + public function testSnapshotWithTimestamp() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION, 'readTimestamp' => self::TIMESTAMP]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snap = $this->operation->snapshot($this->session); + $this->assertInstanceOf(Snapshot::class, $snap); + $this->assertEquals(self::TRANSACTION, $snap->id()); + $this->assertInstanceOf(Timestamp::class, $snap->readTimestamp()); + } + + private function executeAndReadResponse(array $additionalMetadata = []) { return [ - 'metadata' => [ + 'metadata' => array_merge([ 'rowType' => [ 'fields' => [ [ @@ -209,7 +294,7 @@ private function executeAndReadResponse() ] ] ] - ], + ], $additionalMetadata), 'rows' => [ ['10'] ] diff --git a/tests/unit/Spanner/SpannerClientTest.php b/tests/unit/Spanner/SpannerClientTest.php index 17bf7d6406db..ca423786b858 100644 --- a/tests/unit/Spanner/SpannerClientTest.php +++ b/tests/unit/Spanner/SpannerClientTest.php @@ -22,6 +22,7 @@ use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Date; +use Google\Cloud\Spanner\Duration; use Google\Cloud\Spanner\KeyRange; use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Session\SessionClient; @@ -97,6 +98,12 @@ public function testInt64() $this->assertInstanceOf(Int64::class, $i64); } + public function testDuration() + { + $d = $this->client->duration(10, 1); + $this->assertInstanceOf(Duration::class, $d); + } + public function testSessionClient() { $sc = $this->client->sessionClient(); diff --git a/tests/unit/Spanner/TransactionConfigurationTraitTest.php b/tests/unit/Spanner/TransactionConfigurationTraitTest.php new file mode 100644 index 000000000000..2dd3f408e0b3 --- /dev/null +++ b/tests/unit/Spanner/TransactionConfigurationTraitTest.php @@ -0,0 +1,185 @@ +impl = new TransactionConfigurationTraitImplementation; + $this->ts = new Timestamp(new \DateTime(self::TIMESTAMP), self::NANOS); + $this->duration = new Duration(10,1); + $this->dur = ['seconds' => 10, 'nanos' => 1]; + } + + public function testTransactionSelectorBasicSnapshot() + { + $args = []; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertTrue($res[0]['singleUse']['readOnly']['strong']); + } + + public function testTransactionSelectorExistingId() + { + $args = ['transactionId' => self::TRANSACTION]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertEquals(self::TRANSACTION, $res[0]['id']); + } + + public function testTransactionSelectorReadWrite() + { + $args = ['transactionType' => SessionPoolInterface::CONTEXT_READWRITE]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $res[1]); + $this->assertEquals($this->impl->proxyConfigureTransactionOptions(), $res[0]['singleUse']); + } + + public function testBegin() + { + $args = ['begin' => true]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertTrue($res[0]['begin']['readOnly']['strong']); + } + + public function testConfigureSnapshotOptionsReturnReadTimestamp() + { + $args = ['returnReadTimestamp' => true]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertTrue($res['readOnly']['returnReadTimestamp']); + } + + public function testConfigureSnapshotOptionsStrong() + { + $args = ['strong' => true]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertTrue($res['readOnly']['strong']); + } + + public function testConfigureSnapshotOptionsMinReadTimestamp() + { + $args = ['minReadTimestamp' => $this->ts]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals(self::TIMESTAMP, $res['readOnly']['minReadTimestamp']); + } + + public function testConfigureSnapshotOptionsReadTimestamp() + { + $args = ['readTimestamp' => $this->ts]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals(self::TIMESTAMP, $res['readOnly']['readTimestamp']); + } + + public function testConfigureSnapshotOptionsMaxStaleness() + { + $args = ['maxStaleness' => $this->duration]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals($this->dur, $res['readOnly']['maxStaleness']); + } + + public function testConfigureSnapshotOptionsExactStaleness() + { + $args = ['exactStaleness' => $this->duration]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals($this->dur, $res['readOnly']['exactStaleness']); + } + + /** + * @expectedException BadMethodCallException + */ + public function testTransactionSelectorInvalidContext() + { + $args = ['transactionType' => 'foo']; + $this->impl->proxyTransactionSelector($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidExactStaleness() + { + $args = ['exactStaleness' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidMaxStaleness() + { + $args = ['maxStaleness' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidMinReadTimestamp() + { + $args = ['minReadTimestamp' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidReadTimestamp() + { + $args = ['readTimestamp' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } +} + +class TransactionConfigurationTraitImplementation +{ + use TransactionConfigurationTrait; + + public function proxyTransactionSelector(array &$options) + { + return $this->transactionSelector($options); + } + + public function proxyConfigureTransactionOptions() + { + return $this->configureTransactionOptions(); + } + + public function proxyConfigureSnapshotOptions(array &$options) + { + return $this->configureSnapshotOptions($options); + } +} From 9d20984f976ec8102510ccf7f01f2e3a5225c0de Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 27 Jan 2017 11:38:19 -0500 Subject: [PATCH 050/107] Update documentation --- src/Spanner/Database.php | 152 ++++++++++++++++++++++++++++------ src/Spanner/Result.php | 40 ++++++--- src/Spanner/SpannerClient.php | 13 +++ src/Spanner/Transaction.php | 28 ++++--- 4 files changed, 189 insertions(+), 44 deletions(-) diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 7518bf807f10..22eecdf79b45 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -334,17 +334,16 @@ public function iam() * * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest - * @param callable $operation The operations to run in the transaction. - * **Signature:** `function (Transaction $transaction)`. + * @see https://cloud.google.com/spanner/docs/transactions Transactions + * * @param array $options [optional] { * Configuration Options * * See [ReadOnly](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions.ReadOnly) * for detailed description of available options. * - * Please note that only one of `$strong`, `$minReadTimestamp`, - * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in - * a request. + * 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 @@ -356,8 +355,8 @@ public function iam() * @type Duration $exactStaleness Represents a number of seconds. Executes * all reads at a timestamp that is $exactStaleness old. * } - * @codingStandardsIgnoreEnd * @return Snapshot + * @codingStandardsIgnoreEnd */ public function snapshot(array $options = []) { @@ -383,6 +382,22 @@ public function snapshot(array $options = []) * failure, all transaction operations, including reads, are re-applied in a * new transaction. * + * If a transaction exceeds the maximum number of retries, + * {@see Google\Cloud\Exception\AbortedException} will be thrown. Any other + * exception types will immediately bubble up and will interrupt the retry + * operation. + * + * Please note that once a transaction reads data, it will lock the read + * data, preventing other users from modifying that data. For this reason, + * it is important that every transaction commits or rolls back as early as + * possible. Do not hold transactions open longer than necessary. + * + * If you have an active transaction which was obtained from elsewhere, you + * can provide it to this method and gain the benefits of managed retry by + * setting `$options.transaction` to your {@see Google\Cloud\Spanner\Transaction} + * instance. Please note that in this case, it is important that ALL reads + * and mutations MUST be performed within the runTransaction callable. + * * Example: * ``` * $transaction = $database->runTransaction(function (Transaction $t) use ($userName, $password) { @@ -398,6 +413,8 @@ public function snapshot(array $options = []) * * $user['loginCount'] = $user['loginCount'] + 1; * $t->update('Users', $user); + * } else { + * $t->rollback(); * } * * $t->commit(); @@ -406,6 +423,7 @@ public function snapshot(array $options = []) * * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions * @codingStandardsIgnoreEnd * * @param callable $operation The operations to run in the transaction. @@ -414,25 +432,35 @@ public function snapshot(array $options = []) * Configuration Options * * @type int $maxRetries The number of times to attempt to apply the - * operation before failing. **Defaults to ** `4`. + * operation before failing. **Defaults to ** `3`. + * @type Transaction $transaction If provided, the transaction will be + * passed to the callable instead of attempting to begin a new + * transaction. * } - * @return Transaction + * @return mixed The return value of `$operation`. */ public function runTransaction(callable $operation, array $options = []) { $options += [ - 'maxRetries' => self::MAX_RETRIES + 'maxRetries' => self::MAX_RETRIES, + 'transaction' => null ]; // There isn't anything configurable here. - $options['transactionOptions'] = [ - 'readWrite' => [] - ]; + $options['transactionOptions'] = $this->configureTransactionOptions(); $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); - $startTransactionFn = function ($session, $options) { - return $this->operation->transaction($session, $options); + $attempt = 0; + $startTransactionFn = function ($session, $options) use ($options, &$attempt) { + if ($attempt === 0 && $options['transaction'] instanceof Transaction) { + $transaction = $options['transaction']; + } else { + $transaction = $this->operation->transaction($session, $options); + } + + $attempt++; + return $transaction; }; $delayFn = function (\Exception $e) { @@ -445,7 +473,6 @@ public function runTransaction(callable $operation, array $options = []) }; $commitFn = function($operation, $session, $options) use ($startTransactionFn) { - $transaction = call_user_func_array($startTransactionFn, [ $session, $options @@ -468,6 +495,11 @@ public function runTransaction(callable $operation, array $options = []) * If you wish Google Cloud PHP to handle retry logic for you (recommended * for most cases), use {@see Google\Cloud\Spanner\Database::runTransaction()}. * + * Please note that once a transaction reads data, it will lock the read + * data, preventing other users from modifying that data. For this reason, + * it is important that every transaction commits or rolls back as early as + * possible. Do not hold transactions open longer than necessary. + * * Example: * ``` * $transaction = $database->transaction(); @@ -475,6 +507,7 @@ public function runTransaction(callable $operation, array $options = []) * * @codingStandardsIgnoreStart * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions * @codingStandardsIgnoreEnd * * @param array $options [optional] Configuration Options. @@ -494,6 +527,8 @@ public function transaction(array $options = []) /** * Insert a row. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $database->insert('Posts', [ @@ -520,6 +555,8 @@ public function insert($table, array $data, array $options = []) /** * Insert multiple rows. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $database->insert('Posts', [ @@ -564,6 +601,8 @@ public function insertBatch($table, array $dataSet, array $options = []) * enough information for the API to determine which row should be modified. * In most cases, this means providing values for the Primary Key fields. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $database->update('Posts', [ @@ -593,6 +632,8 @@ public function update($table, array $data, array $options = []) * enough information for the API to determine which row should be modified. * In most cases, this means providing values for the Primary Key fields. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $database->update('Posts', [ @@ -635,6 +676,8 @@ public function updateBatch($table, array $dataSet, array $options = []) * existing table data), the row will be updated. If not, it will be * created. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $database->insertOrUpdate('Posts', [ @@ -665,6 +708,8 @@ public function insertOrUpdate($table, array $data, array $options = []) * existing table data), the row will be updated. If not, it will be * created. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $database->insertOrUpdateBatch('Posts', [ @@ -709,6 +754,8 @@ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) * find a record matching the Primary Key, and will replace the entire row. * If a matching row is not found, it will be inserted. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $database->replace('Posts', [ @@ -739,6 +786,8 @@ public function replace($table, array $data, array $options = []) * find a record matching the Primary Key, and will replace the entire row. * If a matching row is not found, it will be inserted. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $database->replaceBatch('Posts', [ @@ -779,6 +828,8 @@ public function replaceBatch($table, array $dataSet, array $options = []) /** * Delete one or more rows. * + * Mutations are committed in a single-use transaction. + * * Example: * ``` * $keySet = $spanner->keySet([ @@ -814,14 +865,36 @@ public function delete($table, KeySet $keySet, array $options = []) * * Example: * ``` - * $result = $spanner->execute( - * 'SELECT * FROM Posts WHERE ID = @postId', - * [ - * 'parameters' => [ - * 'postId' => 1337 - * ] + * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 * ] - * ); + * ]); + * ``` + * + * ``` + * // Execute a read and return a new Snapshot for further reads. + * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 + * ], + * 'begin' => true + * ]); + * + * $snapshot = $result->snapshot(); + * ``` + * + * ``` + * // Execute a read and return a new Transaction for further reads and writes. + * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 + * ], + * 'begin' => true, + * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE + * ]); + * + * $transaction = $result->transaction(); * ``` * * @codingStandardsIgnoreStart @@ -851,7 +924,7 @@ public function delete($table, KeySet $keySet, array $options = []) * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the * given timestamp. Only available in single-use transactions. * @type Duration $maxStaleness Read data at a timestamp >= NOW - the - * given timestamp. + * given timestamp. Only available in single-use transactions. * @type Timestamp $readTimestamp Executes all reads at the given * timestamp. * @type Duration $exactStaleness Represents a number of seconds. Executes @@ -898,6 +971,37 @@ public function execute($sql, array $options = []) * $result = $database->read('Posts', $keySet, $columns); * ``` * + * ``` + * // Execute a read and return a new Snapshot for further reads. + * $keySet = $spanner->keySet([ + * 'keys' => [1337] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns, [ + * 'begin' => true + * ]); + * + * $snapshot = $result->snapshot(); + * ``` + * + * ``` + * // Execute a read and return a new Transaction for further reads and writes. + * $keySet = $spanner->keySet([ + * 'keys' => [1337] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns, [ + * 'begin' => true, + * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE + * ]); + * + * $transaction = $result->transaction(); + * ``` + * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest ReadRequest * * @codingStandardsIgnoreStart @@ -925,7 +1029,7 @@ public function execute($sql, array $options = []) * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the * given timestamp. Only available in single-use transactions. * @type Duration $maxStaleness Read data at a timestamp >= NOW - the - * given timestamp. + * given timestamp. Only available in single-use transactions. * @type Timestamp $readTimestamp Executes all reads at the given * timestamp. * @type Duration $exactStaleness Represents a number of seconds. Executes diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index 6bdb6716a429..c168438ce963 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -143,22 +143,15 @@ public function stats() } /** - * Get the entire query or read response as given by the API. + * Returns a transaction which was begun in the read or execute, if one exists. * * Example: * ``` - * $info = $result->info(); + * $transaction = $result->transaction(); * ``` * - * @codingStandardsIgnoreStart - * @return array [ResultSet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet). - * @codingStandardsIgnoreEnd + * @return Transaction|null */ - public function info() - { - return $this->result; - } - public function transaction() { return (isset($this->options['transaction'])) @@ -166,6 +159,16 @@ public function transaction() : null; } + /** + * Returns a snapshot which was begun in the read or execute, if one exists. + * + * Example: + * ``` + * $snapshot = $result->snapshot(); + * ``` + * + * @return Snapshot|null + */ public function snapshot() { return (isset($this->options['snapshot'])) @@ -173,6 +176,23 @@ public function snapshot() : null; } + /** + * Get the entire query or read response as given by the API. + * + * Example: + * ``` + * $info = $result->info(); + * ``` + * + * @codingStandardsIgnoreStart + * @return array [ResultSet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet). + * @codingStandardsIgnoreEnd + */ + public function info() + { + return $this->result; + } + /** * @access private */ diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 5f74940e4f66..0c2e057ffb38 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -419,6 +419,19 @@ public function int64($value) return new Int64($value); } + /** + * Create a Duration object. + * + * Example: + * ``` + * $duration = $spanner->duration(100, 00001); + * ``` + * + * @param int $seconds The number of seconds in the duration. + * @param int $nanos [optional] The number of nanoseconds in the duration. + * **Defaults to** `0`. + * @return Duration + */ public function duration($seconds, $nanos = 0) { return new Duration($seconds, $nanos); diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index aa275700f7ca..d3bfa0b0e724 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -334,6 +334,24 @@ public function rollback(array $options = []) return $this->operation->rollback($this->session, $this->transactionId, $options); } + /** + * Commit and end the transaction. + * + * It is advised that transactions be run inside + * {@see Google\Cloud\Spanner\Database::runTransaction()} in order to take + * advantage of automated transaction retry in case of a transaction aborted + * error. + * + * Example: + * ``` + * $transaction->commit(); + * ``` + * + * @param array $options [optional] Configuration Options. + * @return Timestamp The commit timestamp. + * @throws \RuntimeException If the transaction is not active + * @throws \AbortedException If the commit is aborted for any reason. + */ public function commit(array $options = []) { if ($this->state !== self::STATE_ACTIVE) { @@ -364,16 +382,6 @@ public function state() return $this->state; } - /** - * Retrieve a list of formatted mutations. - * - * @return array - */ - public function mutations() - { - return $this->mutations; - } - /** * Format, validate and enqueue mutations in the transaction. * From fb94b0d86aa2b292766b838216afa1f7d159f71c Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Fri, 3 Feb 2017 15:17:18 -0500 Subject: [PATCH 051/107] WIP - Long Running Operations client --- .../LongRunningConnectionInterface.php | 19 +-- src/LongRunning/LongRunningOperation.php | 159 ++++++++++++++++++ src/LongRunning/Normalizer/GrpcNormalizer.php | 73 ++++++++ .../LongRunningNormalizerInterface.php | 40 +++++ src/LongRunning/Operation.php | 75 --------- .../Admin/Database/V1/DatabaseAdminClient.php | 2 +- .../Admin/Instance/V1/InstanceAdminClient.php | 2 +- .../Connection/ConnectionInterface.php | 5 + src/Spanner/Connection/Grpc.php | 55 +++++- .../Connection/LongRunningConnection.php | 25 +-- src/Spanner/Database.php | 24 ++- src/Spanner/Instance.php | 24 ++- src/Spanner/SpannerClient.php | 18 +- 13 files changed, 396 insertions(+), 125 deletions(-) create mode 100644 src/LongRunning/LongRunningOperation.php create mode 100644 src/LongRunning/Normalizer/GrpcNormalizer.php create mode 100644 src/LongRunning/Normalizer/LongRunningNormalizerInterface.php delete mode 100644 src/LongRunning/Operation.php diff --git a/src/LongRunning/LongRunningConnectionInterface.php b/src/LongRunning/LongRunningConnectionInterface.php index 3a871a3a107e..ad236ee0addd 100644 --- a/src/LongRunning/LongRunningConnectionInterface.php +++ b/src/LongRunning/LongRunningConnectionInterface.php @@ -19,18 +19,13 @@ interface LongRunningConnectionInterface { - /** - * @param array $args - */ - public function getOperation(array $args); + public function reload(array $args); - /** - * @param array $args - */ - public function cancelOperation(array $args); + public function get(array $args); - /** - * @param array $args - */ - public function deleteOperation(array $args); + public function cancel(array $args); + + public function delete(array $args); + + public function list(array $args); } diff --git a/src/LongRunning/LongRunningOperation.php b/src/LongRunning/LongRunningOperation.php new file mode 100644 index 000000000000..1eff365e4038 --- /dev/null +++ b/src/LongRunning/LongRunningOperation.php @@ -0,0 +1,159 @@ +connection = $connection; + $this->normalizer = $normalizer; + $this->name = $name; + $this->method = $method; + + if (is_null($doneCallback)) { + $this->doneCallback = function($res) { return $res; }; + } else { + $this->doneCallback = $doneCallback; + } + } + + public function name() + { + return $this->name; + } + + public function done() + { + return (isset($this->info['done'])) + ? $this->info['done'] + : false; + } + + public function result() + { + return $this->result; + } + + public function info(array $options = []) + { + return $this->info ?: $this->reload($options); + } + + public function reload(array $options = []) + { + $res = $this->connection->reload([ + 'name' => $this->name, + 'method' => $this->method + ] + $options); + + $this->result = $this->executeDoneCallback($this->normalizer->serializeResult($res)); + return $this->info = $this->normalizer->serializeOperation($res); + } + + public function wait(array $options = []) + { + $isComplete = $this->done(); + + do { + $res = $this->reload($options); + $isComplete = $this->info['done']; + } while(!$isComplete); + + return $this->result; + } + + public function cancel(array $options = []) + { + $this->connection->cancel([ + 'name' => $this->name + ]); + } + + public function delete(array $options = []) + { + $this->connection->delete([ + 'name' => $this->name + ]); + } + + private function executeDoneCallback($res) + { + if (is_null($res)) { + return null; + } + + return call_user_func($this->doneCallback, $res); + } + + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'normalizer' => get_class($this->normalizer), + 'name' => $this->name, + 'method' => $this->method + ]; + } +} diff --git a/src/LongRunning/Normalizer/GrpcNormalizer.php b/src/LongRunning/Normalizer/GrpcNormalizer.php new file mode 100644 index 000000000000..63cb74a5c534 --- /dev/null +++ b/src/LongRunning/Normalizer/GrpcNormalizer.php @@ -0,0 +1,73 @@ +connection = $connection; + $this->codec = new PhpArray(); + } + + /** + * @param mixed $operation + * @return LongRunningOperation + */ + public function normalize($operation, $method, callable $doneCallback = null) + { + if (!($operation instanceof OperationResponse)) { + throw new \BadMethodCallException('operation must be an instance of OperationResponse'); + } + + return new LongRunningOperation($this->connection, $this, $operation->getName(), $method, $doneCallback); + } + + /** + * @param mixed $result + * @return array + */ + public function serializeOperation($result) + { + return $result->getLastProtoResponse()->serialize($this->codec); + } + + /** + * @param mixed $result + * @return array + */ + public function serializeResult($result) + { + return $result->getResult()->serialize($this->codec); + } + + /** + * @param mixed $error + * @return array + */ + public function serializeError($error) + {} +} diff --git a/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php b/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php new file mode 100644 index 000000000000..2d2d35f55695 --- /dev/null +++ b/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php @@ -0,0 +1,40 @@ +connection = $connection; - $this->name = $name; - $this->type = $type; - $this->info = $info; - } - - public function name() - { - return $this->name; - } - - public function reload(array $options = []) - { - $this->info = $info = $this->connection->getOperation([ - 'name' => $this->name, - 'type' => $this->type - ] + $options); - - return $info; - } - - public function info(array $options = []) - { - return $this->info ?: $this->reload($options); - } - - public function wait(array $options = []) - { - do { - $this->reload($options); - } while(true); - } - - public function cancel(array $options = []) - { - return $this->connection->cancelOperation([ - 'name' => $this->name - ] + $options); - } - - public function delete(array $options = []) - { - return $this->connection->deleteOperation([ - 'name' => $this->name - ] + $options); - } -} diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php index 332d10cc7728..83284ea46f2c 100644 --- a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php @@ -235,7 +235,7 @@ private static function getPageStreamingDescriptors() return $pageStreamingDescriptors; } - private static function getLongRunningDescriptors() + public static function getLongRunningDescriptors() { return [ 'createDatabase' => [ diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php index bb7d0a10b257..c64fbb7840ae 100644 --- a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -281,7 +281,7 @@ private static function getPageStreamingDescriptors() return $pageStreamingDescriptors; } - private static function getLongRunningDescriptors() + public static function getLongRunningDescriptors() { return [ 'createInstance' => [ diff --git a/src/Spanner/Connection/ConnectionInterface.php b/src/Spanner/Connection/ConnectionInterface.php index 99d005a3b7d6..b30bfc20e612 100644 --- a/src/Spanner/Connection/ConnectionInterface.php +++ b/src/Spanner/Connection/ConnectionInterface.php @@ -163,4 +163,9 @@ public function cancelOperation(array $args); * @param array $args */ public function deleteOperation(array $args); + + /** + * @param array $args + */ + public function listOperations(array $args); } diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index ae458e1352a8..b9e5a4a72783 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -38,14 +38,6 @@ class Grpc implements ConnectionInterface { use GrpcTrait; - const DATABASE_LRO_TYPE = ''; - const OPERATION_LRO_TYPE = ''; - - private $lroTypes = [ - self::DATABASE_LRO_TYPE, - self::OPERATION_LRO_TYPE - ]; - /** * @var InstanceAdminClient */ @@ -501,12 +493,28 @@ public function rollback(array $args = []) ]); } + /** + * @param array $args + */ + public function reloadOperation(array $args) + { + $name = $this->pluck('name', $args); + $method = $this->pluck('method', $args); + + $operation = $this->getOperationByNameAndMethod($name, $method); + $operation->reload(); + return $operation; + } + /** * @param array $args */ public function getOperation(array $args) { + $name = $this->pluck('name', $args); + $method = $this->pluck('method', $args); + return $this->getOperationByNameAndMethod($name, $method); } /** @@ -514,7 +522,10 @@ public function getOperation(array $args) */ public function cancelOperation(array $args) { + $name = $this->pluck('name', $args); + $method = $this->pluck('method', $args); + $operation = $this->getOperationByNameAndMethod($name, $method); } /** @@ -522,7 +533,35 @@ public function cancelOperation(array $args) */ public function deleteOperation(array $args) { + $name = $this->pluck('name', $args); + $method = $this->pluck('method', $args); + + $operation = $this->getOperationByNameAndMethod($name, $method); + } + + /** + * @param array $args + */ + public function listOperations(array $args) + { + $name = $this->pluck('name', $args); + $method = $this->pluck('method', $args); + } + + private function getOperationByNameAndMethod($name, $method) + { + $client = null; + if (array_key_exists($method, $this->instanceAdminClient::getLongRunningDescriptors())) { + $client = $this->instanceAdminClient; + } elseif (array_key_exists($method, $this->databaseAdminClient::getLongRunningDescriptors())) { + $client = $this->databaseAdminClient; + } + + if (is_null($client)) { + throw new \BadMethodCallException('Invalid LRO method'); + } + return $client->resumeOperation($name, $method); } /** diff --git a/src/Spanner/Connection/LongRunningConnection.php b/src/Spanner/Connection/LongRunningConnection.php index 43e18584d8f2..19b010676ea4 100644 --- a/src/Spanner/Connection/LongRunningConnection.php +++ b/src/Spanner/Connection/LongRunningConnection.php @@ -28,27 +28,28 @@ public function __construct(ConnectionInterface $connection) $this->connection = $connection; } - /** - * @param array $args - */ - public function getOperation(array $args) + public function reload(array $args) + { + return $this->connection->reloadOperation($args); + } + + public function get(array $args) { return $this->connection->getOperation($args); } - /** - * @param array $args - */ - public function cancelOperation(array $args) + public function cancel(array $args) { return $this->connection->cancelOperation($args); } - /** - * @param array $args - */ - public function deleteOperation(array $args) + public function delete(array $args) { return $this->connection->deleteOperation($args); } + + public function list(array $args) + { + return $this->connection->listOperations($args); + } } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index e9c0f5a08550..b0fb2fe9f60e 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -20,7 +20,6 @@ use Google\Cloud\ArrayTrait; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; -use Google\Cloud\LongRunning\LROTrait; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Google\Cloud\Spanner\Session\SessionPoolInterface; @@ -69,6 +68,11 @@ class Database */ private $sessionPool; + /** + * @var LongRunningNormalizerInterface + */ + private $lroNormalizer; + /** * @var Operation */ @@ -95,7 +99,8 @@ class Database * @param ConnectionInterface $connection The connection to the * Google Cloud Spanner Admin API. * @param Instance $instance The instance in which the database exists. - * @param SessionPoolInterface The session pool implementation. + * @param SessionPoolInterface $sessionPool The session pool implementation. + * @param LongRunningNormalizerInterface $lroNormalizer Normalizes LRO interaction. * @param string $projectId The project ID. * @param string $name The database name. * @param bool $returnInt64AsObject If true, 64 bit integers will be @@ -106,13 +111,15 @@ public function __construct( ConnectionInterface $connection, Instance $instance, SessionPoolInterface $sessionPool, + LongRunningNormalizerInterface $lroNormalizer $projectId, $name, - $returnInt64AsObject = false + $returnInt64AsObject ) { $this->connection = $connection; $this->instance = $instance; $this->sessionPool = $sessionPool; + $this->lroNormalizer = $lroNormalizer; $this->projectId = $projectId; $this->name = $name; @@ -189,7 +196,7 @@ public function exists(array $options = []) * * @param string $statement A DDL statement to run against a database. * @param array $options [optional] Configuration options. - * @return + * @return LongRunningOperation */ public function updateDdl($statement, array $options = []) { @@ -224,7 +231,7 @@ public function updateDdl($statement, array $options = []) * * @param string[] $statements A list of DDL statements to run against a database. * @param array $options [optional] Configuration options. - * @return + * @return LongRunningOperation */ public function updateDdlBatch(array $statements, array $options = []) { @@ -232,12 +239,12 @@ public function updateDdlBatch(array $statements, array $options = []) 'operationId' => null ]; - $res = $this->connection->updateDatabase($options + [ + $operation = $this->connection->updateDatabase($options + [ 'name' => $this->fullyQualifiedDatabaseName(), 'statements' => $statements, ]); - return $this->longRunningResponse($res); + return $this->lroNormalizer->normalize($operation, 'updateDatabaseDdl'); } /** @@ -855,8 +862,7 @@ public function __debugInfo() 'projectId' => $this->projectId, 'name' => $this->name, 'instance' => $this->instance, - 'sessionPool' => $this->sessionPool, - 'returnInt64AsObject' => $this->returnInt64AsObject, + 'sessionPool' => $this->sessionPool ]; } } diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 622d982c4ea0..c257cc90a58d 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -19,6 +19,7 @@ use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; +use Google\Cloud\LongRunning\Normalizer\LongRunningNormalizerInterface; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; @@ -54,6 +55,11 @@ class Instance */ private $sessionPool; + /** + * @var LongRunningNormalizerInterface + */ + private $lroNormalizer; + /** * @var string */ @@ -85,6 +91,7 @@ class Instance * @param ConnectionInterface $connection The connection to the * Google Cloud Spanner Admin API. * @param SessionPoolInterface $sessionPool The session pool implementation. + * @param LongRunningNormalizerInterface $lroNormalizer Normalizes Long Running Operations. * @param string $projectId The project ID. * @param string $name The instance name. * @param bool $returnInt64AsObject If true, 64 bit integers will be @@ -95,6 +102,7 @@ class Instance public function __construct( ConnectionInterface $connection, SessionPoolInterface $sessionPool, + LongRunningNormalizerInterface $lroNormalizer, $projectId, $name, $returnInt64AsObject = false, @@ -102,6 +110,7 @@ public function __construct( ) { $this->connection = $connection; $this->sessionPool = $sessionPool; + $this->lroNormalizer = $lroNormalizer; $this->projectId = $projectId; $this->name = $name; $this->returnInt64AsObject = $returnInt64AsObject; @@ -251,7 +260,7 @@ public function state(array $options = []) * @type array $labels For more information, see * [Using labels to organize Google Cloud Platform resources](https://goo.gl/xmQnxf). * } - * @return void + * @return LongRunningOperation * @throws \InvalidArgumentException */ public function update(array $options = []) @@ -266,9 +275,13 @@ public function update(array $options = []) : [] ]; - $this->connection->updateInstance([ + $operation = $this->connection->updateInstance([ 'name' => $this->fullyQualifiedInstanceName(), ] + $options); + + return $this->lroNormalizer->normalize($operation, 'updateInstance', function($result) use ($name) { + return $this->instance($name, $result)); + }); } /** @@ -321,13 +334,15 @@ public function createDatabase($name, array $options = []) $statement = sprintf('CREATE DATABASE `%s`', $name); - $this->connection->createDatabase([ + $operation = $this->connection->createDatabase([ 'instance' => $this->fullyQualifiedInstanceName(), 'createStatement' => $statement, 'extraStatements' => $options['statements'] ]); - return $this->database($name); + return $this->lroNormalizer->normalize($operation, 'createDatabase', function($result) use ($name) { + return $this->database($name); + }); } /** @@ -347,6 +362,7 @@ public function database($name) $this->connection, $this, $this->sessionPool, + $this->lroNormalizer, $this->projectId, $name, $this->returnInt64AsObject diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 55c169534676..de9e06e826be 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -20,8 +20,10 @@ use Google\Cloud\ClientTrait; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Int64; +use Google\Cloud\LongRunning\Normalizer\GrpcNormalizer; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\Grpc; +use Google\Cloud\Spanner\Connection\LongRunningConnection; use Google\Cloud\Spanner\Session\SessionClient; use Google\Cloud\Spanner\Session\SimpleSessionPool; use Google\Cloud\ValidateTrait; @@ -78,6 +80,11 @@ class SpannerClient */ private $returnInt64AsObject; + /** + * @var LongRunningNormalizerInterface + */ + private $lroNormalizer; + /** * Create a Spanner client. * @@ -115,6 +122,8 @@ public function __construct(array $config = []) ]; $this->connection = new Grpc($this->configureAuthentication($config)); + $lroConnection = new LongRunningConnection($this->connection); + $this->lroNormalizer = new GrpcNormalizer($lroConnection); $this->sessionClient = new SessionClient($this->connection, $this->projectId); $this->sessionPool = new SimpleSessionPool($this->sessionClient); @@ -207,7 +216,7 @@ public function configuration($name, array $config = []) * @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 Instance + * @return LongRunningOperation * @codingStandardsIgnoreEnd */ public function createInstance(Configuration $config, $name, array $options = []) @@ -221,14 +230,16 @@ public function createInstance(Configuration $config, $name, array $options = [] // This must always be set to CREATING, so overwrite anything else. $options['state'] = State::CREATING; - $res = $this->connection->createInstance([ + $operation = $this->connection->createInstance([ 'instanceId' => $name, 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) ] + $options); - return $this->instance($name); + return $this->lroNormalizer->normalize($operation, 'createInstance', function($result) use ($name) { + return $this->instance($name, $result)); + }); } /** @@ -247,6 +258,7 @@ public function instance($name, array $instance = []) return new Instance( $this->connection, $this->sessionPool, + $this->lroNormalizer, $this->projectId, $name, $this->returnInt64AsObject, From d7fa65037010b35a6e10e8e6817fc3bec9abd1ae Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 6 Feb 2017 09:45:18 -0500 Subject: [PATCH 052/107] Fix doc --- src/Spanner/Duration.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Spanner/Duration.php b/src/Spanner/Duration.php index 6b489b90cc96..d8c0b45c510d 100644 --- a/src/Spanner/Duration.php +++ b/src/Spanner/Duration.php @@ -33,8 +33,8 @@ * ``` * * ``` - * // Date objects can be cast to strings for easy display. - * echo (string) $date; + * // Duration objects can be cast to strings for easy display. + * echo (string) $duration; * ``` */ class Duration implements ValueInterface From 115300a818bfa8ba511c3311db3f05e996ff629d Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Mon, 6 Feb 2017 09:44:43 -0500 Subject: [PATCH 053/107] WIP --- src/LongRunning/LongRunningOperation.php | 147 +++++++++++++++++- src/LongRunning/Normalizer/GrpcNormalizer.php | 38 ++++- .../LongRunningNormalizerInterface.php | 4 + 3 files changed, 178 insertions(+), 11 deletions(-) diff --git a/src/LongRunning/LongRunningOperation.php b/src/LongRunning/LongRunningOperation.php index 1eff365e4038..11f1d4d8faad 100644 --- a/src/LongRunning/LongRunningOperation.php +++ b/src/LongRunning/LongRunningOperation.php @@ -21,6 +21,13 @@ class LongRunningOperation { + const MAX_RELOADS = 10; + const WAIT_INTERVAL = 1000; + + const STATE_IN_PROGRESS = 'inProgress'; + const STATE_SUCCESS = 'success'; + const STATE_ERROR = 'error'; + /** * @param LongRunningConnectionInterface */ @@ -64,7 +71,7 @@ public function __construct( LongRunningConnectionInterface $connection, NormalizerInterface $normalizer, $name, - $method = null, + $method, callable $doneCallback = null ) { $this->connection = $connection; @@ -72,18 +79,36 @@ public function __construct( $this->name = $name; $this->method = $method; - if (is_null($doneCallback)) { - $this->doneCallback = function($res) { return $res; }; - } else { - $this->doneCallback = $doneCallback; - } + $this->doneCallback = (!is_null($doneCallback)) + ? $doneCallback + : function($res) { return $res; }; } + /** + * Return the Operation name. + * + * @return string + */ public function name() { return $this->name; } + /** + * Return the Operation method. + * + * @return string + */ + public function method() + { + return $this->method; + } + + /** + * Check if the Operation is done. + * + * @return bool + */ public function done() { return (isset($this->info['done'])) @@ -91,16 +116,75 @@ public function done() : false; } + /** + * Get the state of the Operation. + * + * Return value will be one of `LongRunningOperation::STATE_IN_PROGRESS`, + * `LongRunningOperation::STATE_SUCCESS` or + * `LongRunningOperation::STATE_ERROR`. + * + * @return string + */ + public function state() + { + if (!$this->done()) { + return self::STATE_IN_PROGRESS; + } + + if (isset($this->info['response'])) { + return self::STATE_SUCCESS; + } + + return self::STATE_ERROR; + } + + /** + * Get the Operation result. + * + * The return type of this method is dictated by the type of Operation. + * + * Returns null if the Operation is not yet complete, or if an error occurred. + * + * @return mixed|null + */ public function result() { return $this->result; } + /** + * Get the Operation error. + * + * Returns null if the Operation is not yet complete, or if no error occurred. + * + * @return array|null + */ + public function error() + { + return $this->error; + } + + /** + * Get the Operation info. + * + * @codingStandardsIgnoreStart + * @param array $options Configuration options. + * @return array [google.longrunning.Operation](https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operation) + * @codingStandardsIgnoreEnd + */ public function info(array $options = []) { return $this->info ?: $this->reload($options); } + /** + * Reload the Operation to check its status. + * + * @codingStandardsIgnoreStart + * @param array $options Configuration Options. + * @return array [google.longrunning.Operation](https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operation) + * @codingStandardsIgnoreEnd + */ public function reload(array $options = []) { $res = $this->connection->reload([ @@ -112,18 +196,58 @@ public function reload(array $options = []) return $this->info = $this->normalizer->serializeOperation($res); } + /** + * Reload the operation until it is complete. + * + * The return type of this method is dictated by the type of Operation. + * + * @param array $options { + * Configuration Options + * + * @type int $waitInterval The time, in microseconds, to wait between + * checking the status of the Operation. **Defaults to** `1000`. + * @type int $maxReloads The maximum number of reload operations the + * Operation will be checked. In microseconds, the time before + * failure will be `$waitInterval*$maxReloads`. **Defaults to** + * 10. + * } + * @return mixed + * @throws RuntimeException If the max reloads are exceeded. + */ public function wait(array $options = []) { + $options =+ [ + 'waitInterval' => self::WAIT_INTERVAL, + 'maxReloads' => self::MAX_RELOADS + ]; + $isComplete = $this->done(); + $reloads = 0; do { $res = $this->reload($options); - $isComplete = $this->info['done']; + $isComplete = $this->done(); + + if (!$isComplete) { + usleep($options['waitInterval']); + + $reloads++; + if ($reloads > $options['maxReloads']) { + throw new \RuntimeException('The maximum number of Operation reloads has been exceeded.'); + } + } + } while(!$isComplete); return $this->result; } + /** + * Cancel a Long Running Operation. + * + * @param array $options Configuration options. + * @return void + */ public function cancel(array $options = []) { $this->connection->cancel([ @@ -131,6 +255,12 @@ public function cancel(array $options = []) ]); } + /** + * Delete a Long Running Operation. + * + * @param array $options Configuration Options. + * @return void + */ public function delete(array $options = []) { $this->connection->delete([ @@ -147,6 +277,9 @@ private function executeDoneCallback($res) return call_user_func($this->doneCallback, $res); } + /** + * @access private + */ public function __debugInfo() { return [ diff --git a/src/LongRunning/Normalizer/GrpcNormalizer.php b/src/LongRunning/Normalizer/GrpcNormalizer.php index 63cb74a5c534..ec6eaba84a87 100644 --- a/src/LongRunning/Normalizer/GrpcNormalizer.php +++ b/src/LongRunning/Normalizer/GrpcNormalizer.php @@ -22,11 +22,24 @@ use Google\Cloud\PhpArray; use Google\GAX\OperationResponse; +/** + * Normalizes LRO operations performed via GAX. + */ class GrpcNormalizer implements LongRunningNormalizerInterface { + /** + * @var LongRunningConnectionInterface + */ private $connection; + + /** + * @var CodecInterface + */ private $codec; + /** + * @param LongRunningConnectionInterface $connection A connection to an LRO service. + */ public function __construct(LongRunningConnectionInterface $connection) { $this->connection = $connection; @@ -34,7 +47,16 @@ public function __construct(LongRunningConnectionInterface $connection) } /** + * Creates a Long Running Operation instance from an operation. + * + * In gRPC, $operation is an instance of `Google\Gax\OperationResponse`. + * * @param mixed $operation + * @param string $method The API method which created the operation. + * Required by GAX to hydrate an OperationResponse. + * @param callable $doneCallback [optional] A callback, receiving the + * operation result as an array, executed when the operation is + * complete. * @return LongRunningOperation */ public function normalize($operation, $method, callable $doneCallback = null) @@ -47,16 +69,24 @@ public function normalize($operation, $method, callable $doneCallback = null) } /** - * @param mixed $result + * Get the operation response as a php array. + * + * In gRPC, $operation is an instance of `Google\Gax\OperationResponse`. + * + * @param mixed $operation * @return array */ - public function serializeOperation($result) + public function serializeOperation($operation) { - return $result->getLastProtoResponse()->serialize($this->codec); + return $operation->getLastProtoResponse()->serialize($this->codec); } /** - * @param mixed $result + * Get the operation response as a php array. + * + * In gRPC, $operation is an instance of `Google\Gax\OperationResponse`. + * + * @param mixed $operation * @return array */ public function serializeResult($result) diff --git a/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php b/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php index 2d2d35f55695..d11bf8a59a67 100644 --- a/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php +++ b/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php @@ -17,6 +17,10 @@ namespace Google\Cloud\LongRunning\Normalizer; +/** + * Describes an implementation for normalizing LRO operations between different + * API transports. + */ interface LongRunningNormalizerInterface { /** From aff572058773afa5fbc092d292d585eca4cbfda8 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Tue, 7 Feb 2017 16:00:08 -0500 Subject: [PATCH 054/107] Update LRO --- ...ngNormalizerInterface.php => LROTrait.php} | 34 ++---- .../LongRunningConnectionInterface.php | 2 - src/LongRunning/LongRunningOperation.php | 33 +++--- src/LongRunning/Normalizer/GrpcNormalizer.php | 103 ------------------ src/LongRunning/OperationResponseTrait.php | 82 ++++++++++++++ src/Spanner/Connection/Grpc.php | 61 +++++------ .../Connection/LongRunningConnection.php | 5 - src/Spanner/Database.php | 45 ++++++-- src/Spanner/Instance.php | 86 +++++++++++---- src/Spanner/SpannerClient.php | 53 +++++---- 10 files changed, 264 insertions(+), 240 deletions(-) rename src/LongRunning/{Normalizer/LongRunningNormalizerInterface.php => LROTrait.php} (51%) delete mode 100644 src/LongRunning/Normalizer/GrpcNormalizer.php create mode 100644 src/LongRunning/OperationResponseTrait.php diff --git a/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php b/src/LongRunning/LROTrait.php similarity index 51% rename from src/LongRunning/Normalizer/LongRunningNormalizerInterface.php rename to src/LongRunning/LROTrait.php index d11bf8a59a67..ecfe475d8b8b 100644 --- a/src/LongRunning/Normalizer/LongRunningNormalizerInterface.php +++ b/src/LongRunning/LROTrait.php @@ -15,30 +15,16 @@ * limitations under the License. */ -namespace Google\Cloud\LongRunning\Normalizer; +namespace Google\Cloud\LongRunning; -/** - * Describes an implementation for normalizing LRO operations between different - * API transports. - */ -interface LongRunningNormalizerInterface +trait LROTrait { - /** - * @param mixed $operation - * @param string $method - * @return LongRunningOperation - */ - public function normalize($operation, $method); - - /** - * @param mixed $result - * @return array - */ - public function serializeResult($result); - - /** - * @param mixed $error - * @return array - */ - public function serializeError($error); + private function getOperation( + LongRunningConnectionInterface $connection, + $name, + $method, + callable $onDone = null + ) { + return new LongRunningOperation($connection, $name, $method, $onDone); + } } diff --git a/src/LongRunning/LongRunningConnectionInterface.php b/src/LongRunning/LongRunningConnectionInterface.php index ad236ee0addd..ec41f48ce780 100644 --- a/src/LongRunning/LongRunningConnectionInterface.php +++ b/src/LongRunning/LongRunningConnectionInterface.php @@ -19,8 +19,6 @@ interface LongRunningConnectionInterface { - public function reload(array $args); - public function get(array $args); public function cancel(array $args); diff --git a/src/LongRunning/LongRunningOperation.php b/src/LongRunning/LongRunningOperation.php index 11f1d4d8faad..7505026ed651 100644 --- a/src/LongRunning/LongRunningOperation.php +++ b/src/LongRunning/LongRunningOperation.php @@ -17,8 +17,6 @@ namespace Google\Cloud\LongRunning; -use Google\Cloud\LongRunning\Normalizer\NormalizerInterface; - class LongRunningOperation { const MAX_RELOADS = 10; @@ -33,11 +31,6 @@ class LongRunningOperation */ private $connection; - /** - * @param NormalizerInterface - */ - private $normalizer; - /** * @param string */ @@ -59,23 +52,21 @@ class LongRunningOperation private $doneCallback; /** - * @param LongRunningConnectionInterface $connection An implementation mapping to methods which handle LRO - * resolution in the service. - * @param NormalizerInterface $normalizer Normalizes service interaction differences between REST and gRPC. + * @param LongRunningConnectionInterface $connection An implementation + * mapping to methods which handle LRO resolution in the service. * @param string $name The Operation name. - * @param string $method The method used to create the operation. Only applicable when using gRPC transport. - * @param callable $doneCallback [optional] A callback, receiving the operation result as an array, executed when - * the operation is complete. + * @param string $method The method used to create the operation. + * @param callable $doneCallback [optional] A callback, receiving the + * operation result as an array, executed when the operation is + * complete. */ public function __construct( LongRunningConnectionInterface $connection, - NormalizerInterface $normalizer, $name, $method, callable $doneCallback = null ) { $this->connection = $connection; - $this->normalizer = $normalizer; $this->name = $name; $this->method = $method; @@ -187,13 +178,16 @@ public function info(array $options = []) */ public function reload(array $options = []) { - $res = $this->connection->reload([ + $res = $this->connection->get([ 'name' => $this->name, 'method' => $this->method ] + $options); - $this->result = $this->executeDoneCallback($this->normalizer->serializeResult($res)); - return $this->info = $this->normalizer->serializeOperation($res); + if ($res['done']) { + $this->result = $this->executeDoneCallback($res['response']); + } + + return $this->info = $res; } /** @@ -216,7 +210,7 @@ public function reload(array $options = []) */ public function wait(array $options = []) { - $options =+ [ + $options += [ 'waitInterval' => self::WAIT_INTERVAL, 'maxReloads' => self::MAX_RELOADS ]; @@ -284,7 +278,6 @@ public function __debugInfo() { return [ 'connection' => get_class($this->connection), - 'normalizer' => get_class($this->normalizer), 'name' => $this->name, 'method' => $this->method ]; diff --git a/src/LongRunning/Normalizer/GrpcNormalizer.php b/src/LongRunning/Normalizer/GrpcNormalizer.php deleted file mode 100644 index ec6eaba84a87..000000000000 --- a/src/LongRunning/Normalizer/GrpcNormalizer.php +++ /dev/null @@ -1,103 +0,0 @@ -connection = $connection; - $this->codec = new PhpArray(); - } - - /** - * Creates a Long Running Operation instance from an operation. - * - * In gRPC, $operation is an instance of `Google\Gax\OperationResponse`. - * - * @param mixed $operation - * @param string $method The API method which created the operation. - * Required by GAX to hydrate an OperationResponse. - * @param callable $doneCallback [optional] A callback, receiving the - * operation result as an array, executed when the operation is - * complete. - * @return LongRunningOperation - */ - public function normalize($operation, $method, callable $doneCallback = null) - { - if (!($operation instanceof OperationResponse)) { - throw new \BadMethodCallException('operation must be an instance of OperationResponse'); - } - - return new LongRunningOperation($this->connection, $this, $operation->getName(), $method, $doneCallback); - } - - /** - * Get the operation response as a php array. - * - * In gRPC, $operation is an instance of `Google\Gax\OperationResponse`. - * - * @param mixed $operation - * @return array - */ - public function serializeOperation($operation) - { - return $operation->getLastProtoResponse()->serialize($this->codec); - } - - /** - * Get the operation response as a php array. - * - * In gRPC, $operation is an instance of `Google\Gax\OperationResponse`. - * - * @param mixed $operation - * @return array - */ - public function serializeResult($result) - { - return $result->getResult()->serialize($this->codec); - } - - /** - * @param mixed $error - * @return array - */ - public function serializeError($error) - {} -} diff --git a/src/LongRunning/OperationResponseTrait.php b/src/LongRunning/OperationResponseTrait.php new file mode 100644 index 000000000000..587696c68ba0 --- /dev/null +++ b/src/LongRunning/OperationResponseTrait.php @@ -0,0 +1,82 @@ +getLastProtoResponse(); + if (is_null($response)) { + return null; + } + + $response = $response->serialize($codec); + $result = $operation->getResult(); + if (!is_null($result)) { + $result = $result->serialize($codec); + } + + $error = $operation->getError(); + if (!is_null($error)) { + $error = $error->serialize($codec); + } + + $response['response'] = $result; + $response['error'] = $error; + + return $response; + } + + /** + * @param array $clients A list of gRPC Clients with LRO support. + * @param string $name The Operation name. + * @param string $method The method which created the Operation. + * @return OperationResponse + */ + private function getOperationByNameAndMethod(array $clients, $name, $method) + { + $client = null; + foreach ($clients as $client) { + if (!method_exists($client, 'getLongRunningDescriptors')) { + throw new \BadMethodCallException(sprintf( + 'Given client %s does not have a method called `getLongRunningDescriptors`.', + get_class($client) + )); + } + + if (array_key_exists($method, $client::getLongRunningDescriptors())) { + break; + } else { + $client = null; + } + } + + if (is_null($client)) { + throw new \BadMethodCallException('Invalid LRO method'); + } + + return $client->resumeOperation($name, $method); + } +} diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index b9e5a4a72783..2005801019e0 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -20,6 +20,7 @@ use Google\Auth\CredentialsLoader; use Google\Cloud\GrpcRequestWrapper; use Google\Cloud\GrpcTrait; +use Google\Cloud\LongRunning\OperationResponseTrait; use Google\Cloud\PhpArray; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; @@ -37,6 +38,7 @@ class Grpc implements ConnectionInterface { use GrpcTrait; + use OperationResponseTrait; /** * @var InstanceAdminClient @@ -74,6 +76,11 @@ class Grpc implements ConnectionInterface 'delete' => 'setDelete' ]; + /** + * @var array + */ + private $longRunningGrpcClients; + /** * @param array $config [optional] */ @@ -98,6 +105,11 @@ public function __construct(array $config = []) $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); $this->spannerClient = new SpannerClient($grpcConfig); $this->operationsClient = $this->instanceAdminClient->getOperationsClient(); + + $this->longRunningGrpcClients = [ + $this->instanceAdminClient, + $this->databaseAdminClient + ]; } /** @@ -150,12 +162,15 @@ public function getInstance(array $args = []) public function createInstance(array $args = []) { $instance = $this->instanceObject($args, true); - return $this->send([$this->instanceAdminClient, 'createInstance'], [ + + $res = $this->send([$this->instanceAdminClient, 'createInstance'], [ $this->pluck('projectId', $args), $this->pluck('instanceId', $args), $instance, $args ]); + + return $this->operationToArray($res, $this->codec); } /** @@ -169,11 +184,13 @@ public function updateInstance(array $args = []) $fieldMask = (new protobuf\FieldMask())->deserialize(['paths' => $mask], $this->codec); - return $this->send([$this->instanceAdminClient, 'updateInstance'], [ + $res = $this->send([$this->instanceAdminClient, 'updateInstance'], [ $instanceObject, $fieldMask, $args ]); + + return $this->operationToArray($res, $this->codec); } private function instanceObject(array &$args, $required = false) @@ -250,12 +267,14 @@ public function listDatabases(array $args = []) */ public function createDatabase(array $args = []) { - return $this->send([$this->databaseAdminClient, 'createDatabase'], [ + $res = $this->send([$this->databaseAdminClient, 'createDatabase'], [ $this->pluck('instance', $args), $this->pluck('createStatement', $args), $this->pluck('extraStatements', $args), $args ]); + + return $this->operationToArray($res, $this->codec); } /** @@ -263,11 +282,13 @@ public function createDatabase(array $args = []) */ public function updateDatabase(array $args = []) { - return $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [ + $res = $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [ $this->pluck('name', $args), $this->pluck('statements', $args), $args ]); + + return $this->operationToArray($res, $this->codec); } /** @@ -496,25 +517,15 @@ public function rollback(array $args = []) /** * @param array $args */ - public function reloadOperation(array $args) + public function getOperation(array $args) { $name = $this->pluck('name', $args); $method = $this->pluck('method', $args); - $operation = $this->getOperationByNameAndMethod($name, $method); + $operation = $this->getOperationByNameAndMethod($this->longRunningGrpcClients, $name, $method); $operation->reload(); - return $operation; - } - - /** - * @param array $args - */ - public function getOperation(array $args) - { - $name = $this->pluck('name', $args); - $method = $this->pluck('method', $args); - return $this->getOperationByNameAndMethod($name, $method); + return $this->operationToArray($operation, $this->codec); } /** @@ -548,22 +559,6 @@ public function listOperations(array $args) $method = $this->pluck('method', $args); } - private function getOperationByNameAndMethod($name, $method) - { - $client = null; - if (array_key_exists($method, $this->instanceAdminClient::getLongRunningDescriptors())) { - $client = $this->instanceAdminClient; - } elseif (array_key_exists($method, $this->databaseAdminClient::getLongRunningDescriptors())) { - $client = $this->databaseAdminClient; - } - - if (is_null($client)) { - throw new \BadMethodCallException('Invalid LRO method'); - } - - return $client->resumeOperation($name, $method); - } - /** * @param array $keySet * @return array Formatted keyset diff --git a/src/Spanner/Connection/LongRunningConnection.php b/src/Spanner/Connection/LongRunningConnection.php index 19b010676ea4..531c4be43080 100644 --- a/src/Spanner/Connection/LongRunningConnection.php +++ b/src/Spanner/Connection/LongRunningConnection.php @@ -28,11 +28,6 @@ public function __construct(ConnectionInterface $connection) $this->connection = $connection; } - public function reload(array $args) - { - return $this->connection->reloadOperation($args); - } - public function get(array $args) { return $this->connection->getOperation($args); diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index b0fb2fe9f60e..b8c638d8fd43 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -20,6 +20,8 @@ use Google\Cloud\ArrayTrait; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; +use Google\Cloud\LongRunning\LROTrait; +use Google\Cloud\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Google\Cloud\Spanner\Session\SessionPoolInterface; @@ -52,6 +54,7 @@ class Database { use ArrayTrait; + use LROTrait; /** * @var ConnectionInterface @@ -69,9 +72,9 @@ class Database private $sessionPool; /** - * @var LongRunningNormalizerInterface + * @var LongRunningConnectionInterface */ - private $lroNormalizer; + private $lroConnection; /** * @var Operation @@ -100,7 +103,8 @@ class Database * Google Cloud Spanner Admin API. * @param Instance $instance The instance in which the database exists. * @param SessionPoolInterface $sessionPool The session pool implementation. - * @param LongRunningNormalizerInterface $lroNormalizer Normalizes LRO interaction. + * @param LongRunningConnectionInterface $lroConnection An implementation + * mapping to methods which handle LRO resolution in the service. * @param string $projectId The project ID. * @param string $name The database name. * @param bool $returnInt64AsObject If true, 64 bit integers will be @@ -111,7 +115,7 @@ public function __construct( ConnectionInterface $connection, Instance $instance, SessionPoolInterface $sessionPool, - LongRunningNormalizerInterface $lroNormalizer + LongRunningConnectionInterface $lroConnection, $projectId, $name, $returnInt64AsObject @@ -119,7 +123,7 @@ public function __construct( $this->connection = $connection; $this->instance = $instance; $this->sessionPool = $sessionPool; - $this->lroNormalizer = $lroNormalizer; + $this->lroConnection = $lroConnection; $this->projectId = $projectId; $this->name = $name; @@ -230,21 +234,38 @@ 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. + * @param array $options [optional] { + * Configuration options. + * + * @type string $operationName If checking the status of an existing + * update operation, it may be supplied here. Note that if an + * operation name is given, no service requests will be executed. + * } * @return LongRunningOperation */ public function updateDdlBatch(array $statements, array $options = []) { $options += [ - 'operationId' => null + 'operationId' => null, + 'operationName' => null, ]; - $operation = $this->connection->updateDatabase($options + [ - 'name' => $this->fullyQualifiedDatabaseName(), - 'statements' => $statements, - ]); + if (is_null($options['operationName'])) { + $operation = $this->connection->updateDatabase($options + [ + 'name' => $this->fullyQualifiedDatabaseName(), + 'statements' => $statements, + ]); + + $operationName = $operation['name']; + } else { + $operationName = $options['operationName']; + } - return $this->lroNormalizer->normalize($operation, 'updateDatabaseDdl'); + return $this->getOperation( + $this->lroConnection, + $operationName, + 'updateDatabaseDdl' + ); } /** diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index c257cc90a58d..9eee0e883053 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -19,7 +19,8 @@ use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; -use Google\Cloud\LongRunning\Normalizer\LongRunningNormalizerInterface; +use Google\Cloud\LongRunning\LROTrait; +use Google\Cloud\LongRunning\LongRunningConnectionInterface; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\ConnectionInterface; @@ -42,6 +43,8 @@ */ class Instance { + use LROTrait; + const STATE_READY = State::READY; const STATE_CREATING = State::CREATING; @@ -56,9 +59,9 @@ class Instance private $sessionPool; /** - * @var LongRunningNormalizerInterface + * @var LongRunningConnectionInterface */ - private $lroNormalizer; + private $lroConnection; /** * @var string @@ -91,7 +94,8 @@ class Instance * @param ConnectionInterface $connection The connection to the * Google Cloud Spanner Admin API. * @param SessionPoolInterface $sessionPool The session pool implementation. - * @param LongRunningNormalizerInterface $lroNormalizer Normalizes Long Running Operations. + * @param LongRunningConnectionInterface $lroConnection An implementation + * mapping to methods which handle LRO resolution in the service. * @param string $projectId The project ID. * @param string $name The instance name. * @param bool $returnInt64AsObject If true, 64 bit integers will be @@ -102,7 +106,7 @@ class Instance public function __construct( ConnectionInterface $connection, SessionPoolInterface $sessionPool, - LongRunningNormalizerInterface $lroNormalizer, + LongRunningConnectionInterface $lroConnection, $projectId, $name, $returnInt64AsObject = false, @@ -110,7 +114,7 @@ public function __construct( ) { $this->connection = $connection; $this->sessionPool = $sessionPool; - $this->lroNormalizer = $lroNormalizer; + $this->lroConnection = $lroConnection; $this->projectId = $projectId; $this->name = $name; $this->returnInt64AsObject = $returnInt64AsObject; @@ -259,6 +263,9 @@ public function state(array $options = []) * **Defaults to** `1`. * @type array $labels For more information, see * [Using labels to organize Google Cloud Platform resources](https://goo.gl/xmQnxf). + * @type string $operationName If checking the status of an existing + * update operation, it may be supplied here. Note that if an + * operation name is given, no service requests will be executed. * } * @return LongRunningOperation * @throws \InvalidArgumentException @@ -268,6 +275,7 @@ public function update(array $options = []) $info = $this->info($options); $options += [ + 'operationName' => null, 'displayName' => $info['displayName'], 'nodeCount' => (isset($info['nodeCount'])) ? $info['nodeCount'] : null, 'labels' => (isset($info['labels'])) @@ -275,13 +283,32 @@ public function update(array $options = []) : [] ]; - $operation = $this->connection->updateInstance([ - 'name' => $this->fullyQualifiedInstanceName(), - ] + $options); + if (is_null($options['operationName'])) { + $operation = $this->connection->updateInstance([ + 'name' => $this->fullyQualifiedInstanceName(), + ] + $options); + + $operationName = $operation['name']; + } else { + $operationName = $options['operationName']; + } - return $this->lroNormalizer->normalize($operation, 'updateInstance', function($result) use ($name) { - return $this->instance($name, $result)); - }); + return $this->getOperation( + $this->lroConnection, + $operationName, + 'updateInstance', + function($result) { + return new self( + $this->connection, + $this->sessionPool, + $this->lroConnection, + $this->projectId, + $this->name, + $this->returnInt64AsObject, + $result + ); + } + ); } /** @@ -323,26 +350,41 @@ public function delete(array $options = []) * Configuration Options * * @type array $statements Additional DDL statements. + * @type string $operationName If checking the status of an existing + * update operation, it may be supplied here. Note that if an + * operation name is given, no service requests will be executed. * } * @return Database */ public function createDatabase($name, array $options = []) { $options += [ - 'statements' => [] + 'statements' => [], + 'operationName' => null ]; $statement = sprintf('CREATE DATABASE `%s`', $name); - $operation = $this->connection->createDatabase([ - 'instance' => $this->fullyQualifiedInstanceName(), - 'createStatement' => $statement, - 'extraStatements' => $options['statements'] - ]); + if (is_null($options['operationName'])) { + $operation = $this->connection->createDatabase([ + 'instance' => $this->fullyQualifiedInstanceName(), + 'createStatement' => $statement, + 'extraStatements' => $options['statements'] + ]); + + $operationName = $operation['name']; + } else { + $operationName = $options['operationName']; + } - return $this->lroNormalizer->normalize($operation, 'createDatabase', function($result) use ($name) { - return $this->database($name); - }); + return $this->getOperation( + $this->lroConnection, + $operationName, + 'createDatabase', + function($result) use ($name) { + return $this->database($name); + } + ); } /** @@ -362,7 +404,7 @@ public function database($name) $this->connection, $this, $this->sessionPool, - $this->lroNormalizer, + $this->lroConnection, $this->projectId, $name, $this->returnInt64AsObject diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index de9e06e826be..65e3830bdec0 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -20,7 +20,7 @@ use Google\Cloud\ClientTrait; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Int64; -use Google\Cloud\LongRunning\Normalizer\GrpcNormalizer; +use Google\Cloud\LongRunning\LROTrait; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\Grpc; use Google\Cloud\Spanner\Connection\LongRunningConnection; @@ -52,6 +52,7 @@ class SpannerClient { use ClientTrait; + use LROTrait; use ValidateTrait; const FULL_CONTROL_SCOPE = 'https://www.googleapis.com/auth/spanner.data'; @@ -65,6 +66,11 @@ class SpannerClient */ protected $connection; + /** + * @var LongRunningConnectionInterface + */ + private $lroConnection; + /** * @var SessionClient */ @@ -80,11 +86,6 @@ class SpannerClient */ private $returnInt64AsObject; - /** - * @var LongRunningNormalizerInterface - */ - private $lroNormalizer; - /** * Create a Spanner client. * @@ -122,8 +123,7 @@ public function __construct(array $config = []) ]; $this->connection = new Grpc($this->configureAuthentication($config)); - $lroConnection = new LongRunningConnection($this->connection); - $this->lroNormalizer = new GrpcNormalizer($lroConnection); + $this->lroConnection = new LongRunningConnection($this->connection); $this->sessionClient = new SessionClient($this->connection, $this->projectId); $this->sessionPool = new SimpleSessionPool($this->sessionClient); @@ -215,6 +215,9 @@ public function configuration($name, array $config = []) * @type int $nodeCount **Defaults to** `1`. * @type array $labels For more information, see * [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). + * @type string $operationName If checking the status of an existing + * update operation, it may be supplied here. Note that if an + * operation name is given, no service requests will be executed. * } * @return LongRunningOperation * @codingStandardsIgnoreEnd @@ -224,22 +227,34 @@ public function createInstance(Configuration $config, $name, array $options = [] $options += [ 'displayName' => $name, 'nodeCount' => self::DEFAULT_NODE_COUNT, - 'labels' => [] + 'labels' => [], + 'operationName' => null, ]; // This must always be set to CREATING, so overwrite anything else. $options['state'] = State::CREATING; - $operation = $this->connection->createInstance([ - 'instanceId' => $name, - 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), - 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), - 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) - ] + $options); + if (is_null($options['operationName'])) { + $operation = $this->connection->createInstance([ + 'instanceId' => $name, + 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), + 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) + ] + $options); + + $operationName = $operation['name']; + } else { + $operationName = $options['operationName']; + } - return $this->lroNormalizer->normalize($operation, 'createInstance', function($result) use ($name) { - return $this->instance($name, $result)); - }); + return $this->getOperation( + $this->lroConnection, + $operationName, + 'createInstance', + function($result) use ($name) { + return $this->instance($name, $result); + } + ); } /** @@ -258,7 +273,7 @@ public function instance($name, array $instance = []) return new Instance( $this->connection, $this->sessionPool, - $this->lroNormalizer, + $this->lroConnection, $this->projectId, $name, $this->returnInt64AsObject, From 605dda440446cd30abea85444b5d52ce3aaf3253 Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Wed, 8 Feb 2017 16:35:27 -0500 Subject: [PATCH 055/107] LRO with only an operation name! aww yeah --- src/LongRunning/LROTrait.php | 14 ++--- src/LongRunning/LongRunningOperation.php | 58 ++++++++++-------- src/LongRunning/OperationResponseTrait.php | 54 ++++++++-------- src/Spanner/Connection/Grpc.php | 37 ++++++++--- src/Spanner/Database.php | 35 +++-------- src/Spanner/Instance.php | 71 ++++++---------------- src/Spanner/SpannerClient.php | 68 ++++++++++++++------- 7 files changed, 172 insertions(+), 165 deletions(-) diff --git a/src/LongRunning/LROTrait.php b/src/LongRunning/LROTrait.php index ecfe475d8b8b..c32d7270885f 100644 --- a/src/LongRunning/LROTrait.php +++ b/src/LongRunning/LROTrait.php @@ -19,12 +19,12 @@ trait LROTrait { - private function getOperation( - LongRunningConnectionInterface $connection, - $name, - $method, - callable $onDone = null - ) { - return new LongRunningOperation($connection, $name, $method, $onDone); + public function lro($operationName) + { + return new LongRunningOperation( + $this->lroConnection, + $operationName, + $this->lroCallables + ); } } diff --git a/src/LongRunning/LongRunningOperation.php b/src/LongRunning/LongRunningOperation.php index 7505026ed651..58af7f70a9af 100644 --- a/src/LongRunning/LongRunningOperation.php +++ b/src/LongRunning/LongRunningOperation.php @@ -27,52 +27,47 @@ class LongRunningOperation const STATE_ERROR = 'error'; /** - * @param LongRunningConnectionInterface + * @var LongRunningConnectionInterface */ private $connection; /** - * @param string + * @var string */ private $name; /** - * @param array + * @var array */ private $info; /** - * @param string|null + * @var array|null */ - private $method; + private $result; /** - * @param callable|null + * @var array */ - private $doneCallback; + private $callablesMap; /** * @param LongRunningConnectionInterface $connection An implementation * mapping to methods which handle LRO resolution in the service. * @param string $name The Operation name. - * @param string $method The method used to create the operation. - * @param callable $doneCallback [optional] A callback, receiving the - * operation result as an array, executed when the operation is - * complete. + * @param array $callablesMap An collection of form [(string) type, (callable) callable] + * providing a function to invoke when an operation completes. The + * callable Type should correspond to an expected value of + * operation.metadata.typeUrl. */ public function __construct( LongRunningConnectionInterface $connection, $name, - $method, - callable $doneCallback = null + array $callablesMap ) { $this->connection = $connection; $this->name = $name; - $this->method = $method; - - $this->doneCallback = (!is_null($doneCallback)) - ? $doneCallback - : function($res) { return $res; }; + $this->callablesMap = $callablesMap; } /** @@ -180,11 +175,14 @@ public function reload(array $options = []) { $res = $this->connection->get([ 'name' => $this->name, - 'method' => $this->method ] + $options); - if ($res['done']) { - $this->result = $this->executeDoneCallback($res['response']); + if (isset($res['done']) && $res['done']) { + $type = $res['metadata']['typeUrl']; + $this->result = $this->executeDoneCallback($type, $res['response']); + $this->error = (isset($res['error'])) + ? $res['error'] + : null; } return $this->info = $res; @@ -262,13 +260,24 @@ public function delete(array $options = []) ]); } - private function executeDoneCallback($res) + private function executeDoneCallback($type, $response) { - if (is_null($res)) { + if (is_null($response)) { return null; } - return call_user_func($this->doneCallback, $res); + $callables = array_filter($this->callablesMap, function($callable) use ($type) { + return $callable['typeUrl'] === $type; + }); + + if (count($callables) === 0) { + return $response; + } + + $callable = current($callables); + $fn = $callable['callable']; + + return call_user_func($fn, $response); } /** @@ -279,7 +288,6 @@ public function __debugInfo() return [ 'connection' => get_class($this->connection), 'name' => $this->name, - 'method' => $this->method ]; } } diff --git a/src/LongRunning/OperationResponseTrait.php b/src/LongRunning/OperationResponseTrait.php index 587696c68ba0..1ce059de9aad 100644 --- a/src/LongRunning/OperationResponseTrait.php +++ b/src/LongRunning/OperationResponseTrait.php @@ -25,7 +25,7 @@ */ trait OperationResponseTrait { - private function operationToArray(OperationResponse $operation, CodecInterface $codec) + private function operationToArray(OperationResponse $operation, CodecInterface $codec, array $lroMappers) { $response = $operation->getLastProtoResponse(); if (is_null($response)) { @@ -33,9 +33,11 @@ private function operationToArray(OperationResponse $operation, CodecInterface $ } $response = $response->serialize($codec); - $result = $operation->getResult(); - if (!is_null($result)) { - $result = $result->serialize($codec); + + $result = null; + if ($operation->isDone()) { + $type = $response['metadata']['typeUrl']; + $result = $this->deserializeResult($operation, $type, $codec, $lroMappers); } $error = $operation->getError(); @@ -50,33 +52,37 @@ private function operationToArray(OperationResponse $operation, CodecInterface $ } /** - * @param array $clients A list of gRPC Clients with LRO support. + * @param mixed $client A generated client with a `resumeOperation` method. * @param string $name The Operation name. - * @param string $method The method which created the Operation. * @return OperationResponse */ - private function getOperationByNameAndMethod(array $clients, $name, $method) + private function getOperationByName($client, $name) { - $client = null; - foreach ($clients as $client) { - if (!method_exists($client, 'getLongRunningDescriptors')) { - throw new \BadMethodCallException(sprintf( - 'Given client %s does not have a method called `getLongRunningDescriptors`.', - get_class($client) - )); - } - - if (array_key_exists($method, $client::getLongRunningDescriptors())) { - break; - } else { - $client = null; - } + return $client->resumeOperation($name); + } + + private function deserializeResult($operation, $type, $codec, $mappers) + { + $mappers = array_filter($mappers, function($mapper) use ($type) { + return $mapper['typeUrl'] === $type; + }); + + if (count($mappers) === 0) { + throw new \RuntimeException(sprintf('No mapper exists for operation response type %s.', $type)); } - if (is_null($client)) { - throw new \BadMethodCallException('Invalid LRO method'); + $mapper = current($mappers); + $message = $mapper['message']; + + $response = new $message(); + $anyResponse = $operation->getLastProtoResponse()->getResponse(); + + if (is_null($anyResponse)) { + return null; } - return $client->resumeOperation($name, $method); + $response->parse($anyResponse->getValue()); + + return $response->serialize($codec); } } diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index 2005801019e0..859485152820 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -27,6 +27,7 @@ use Google\Cloud\Spanner\V1\SpannerClient; use Google\GAX\ApiException; use google\protobuf; +use \google\spanner\admin\database\v1\Database; use google\spanner\admin\instance\v1\Instance; use google\spanner\admin\instance\v1\State; use google\spanner\v1; @@ -76,6 +77,29 @@ class Grpc implements ConnectionInterface 'delete' => 'setDelete' ]; + /** + * @var array + */ + private $lroResponseMappers = [ + [ + 'method' => 'updateDatabaseDdl', + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.UpdateDatabaseDdlMetadata', + 'message' => protobuf\EmptyC::class + ], [ + 'method' => 'createDatabase', + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata', + 'message' => Database::class + ], [ + 'method' => 'createInstance', + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata', + 'message' => Instance::class + ], [ + 'method' => 'updateInstance', + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', + 'message' => Instance::class + ] + ]; + /** * @var array */ @@ -170,7 +194,7 @@ public function createInstance(array $args = []) $args ]); - return $this->operationToArray($res, $this->codec); + return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); } /** @@ -190,7 +214,7 @@ public function updateInstance(array $args = []) $args ]); - return $this->operationToArray($res, $this->codec); + return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); } private function instanceObject(array &$args, $required = false) @@ -274,7 +298,7 @@ public function createDatabase(array $args = []) $args ]); - return $this->operationToArray($res, $this->codec); + return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); } /** @@ -288,7 +312,7 @@ public function updateDatabase(array $args = []) $args ]); - return $this->operationToArray($res, $this->codec); + return $this->operationToArray($res, $this->codec, $this->lroResponseMappers); } /** @@ -520,12 +544,11 @@ public function rollback(array $args = []) public function getOperation(array $args) { $name = $this->pluck('name', $args); - $method = $this->pluck('method', $args); - $operation = $this->getOperationByNameAndMethod($this->longRunningGrpcClients, $name, $method); + $operation = $this->getOperationByName($this->databaseAdminClient, $name); $operation->reload(); - return $this->operationToArray($operation, $this->codec); + return $this->operationToArray($operation, $this->codec, $this->lroResponseMappers); } /** diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index b8c638d8fd43..762e7ab840de 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -116,6 +116,7 @@ public function __construct( Instance $instance, SessionPoolInterface $sessionPool, LongRunningConnectionInterface $lroConnection, + array $lroCallables, $projectId, $name, $returnInt64AsObject @@ -124,6 +125,7 @@ public function __construct( $this->instance = $instance; $this->sessionPool = $sessionPool; $this->lroConnection = $lroConnection; + $this->lroCallables = $lroCallables; $this->projectId = $projectId; $this->name = $name; @@ -234,38 +236,17 @@ 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. - * - * @type string $operationName If checking the status of an existing - * update operation, it may be supplied here. Note that if an - * operation name is given, no service requests will be executed. - * } + * @param array $options [optional] Configuration options. * @return LongRunningOperation */ public function updateDdlBatch(array $statements, array $options = []) { - $options += [ - 'operationId' => null, - 'operationName' => null, - ]; - - if (is_null($options['operationName'])) { - $operation = $this->connection->updateDatabase($options + [ - 'name' => $this->fullyQualifiedDatabaseName(), - 'statements' => $statements, - ]); - - $operationName = $operation['name']; - } else { - $operationName = $options['operationName']; - } + $operation = $this->connection->updateDatabase($options + [ + 'name' => $this->fullyQualifiedDatabaseName(), + 'statements' => $statements, + ]); - return $this->getOperation( - $this->lroConnection, - $operationName, - 'updateDatabaseDdl' - ); + return $this->lro($operation['name']); } /** diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 9eee0e883053..ff6637a29427 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -63,6 +63,11 @@ class Instance */ private $lroConnection; + /** + * @var array + */ + private $lroCallables; + /** * @var string */ @@ -96,6 +101,7 @@ class Instance * @param SessionPoolInterface $sessionPool The session pool implementation. * @param LongRunningConnectionInterface $lroConnection An implementation * mapping to methods which handle LRO resolution in the service. + * @param array $lroCallables * @param string $projectId The project ID. * @param string $name The instance name. * @param bool $returnInt64AsObject If true, 64 bit integers will be @@ -107,6 +113,7 @@ public function __construct( ConnectionInterface $connection, SessionPoolInterface $sessionPool, LongRunningConnectionInterface $lroConnection, + array $lroCallables, $projectId, $name, $returnInt64AsObject = false, @@ -115,6 +122,7 @@ public function __construct( $this->connection = $connection; $this->sessionPool = $sessionPool; $this->lroConnection = $lroConnection; + $this->lroCallables = $lroCallables; $this->projectId = $projectId; $this->name = $name; $this->returnInt64AsObject = $returnInt64AsObject; @@ -263,9 +271,6 @@ public function state(array $options = []) * **Defaults to** `1`. * @type array $labels For more information, see * [Using labels to organize Google Cloud Platform resources](https://goo.gl/xmQnxf). - * @type string $operationName If checking the status of an existing - * update operation, it may be supplied here. Note that if an - * operation name is given, no service requests will be executed. * } * @return LongRunningOperation * @throws \InvalidArgumentException @@ -275,7 +280,6 @@ public function update(array $options = []) $info = $this->info($options); $options += [ - 'operationName' => null, 'displayName' => $info['displayName'], 'nodeCount' => (isset($info['nodeCount'])) ? $info['nodeCount'] : null, 'labels' => (isset($info['labels'])) @@ -283,32 +287,11 @@ public function update(array $options = []) : [] ]; - if (is_null($options['operationName'])) { - $operation = $this->connection->updateInstance([ - 'name' => $this->fullyQualifiedInstanceName(), - ] + $options); + $operation = $this->connection->updateInstance([ + 'name' => $this->fullyQualifiedInstanceName(), + ] + $options); - $operationName = $operation['name']; - } else { - $operationName = $options['operationName']; - } - - return $this->getOperation( - $this->lroConnection, - $operationName, - 'updateInstance', - function($result) { - return new self( - $this->connection, - $this->sessionPool, - $this->lroConnection, - $this->projectId, - $this->name, - $this->returnInt64AsObject, - $result - ); - } - ); + return $this->lro($operation['name']); } /** @@ -350,9 +333,6 @@ public function delete(array $options = []) * Configuration Options * * @type array $statements Additional DDL statements. - * @type string $operationName If checking the status of an existing - * update operation, it may be supplied here. Note that if an - * operation name is given, no service requests will be executed. * } * @return Database */ @@ -360,31 +340,17 @@ public function createDatabase($name, array $options = []) { $options += [ 'statements' => [], - 'operationName' => null ]; $statement = sprintf('CREATE DATABASE `%s`', $name); - if (is_null($options['operationName'])) { - $operation = $this->connection->createDatabase([ - 'instance' => $this->fullyQualifiedInstanceName(), - 'createStatement' => $statement, - 'extraStatements' => $options['statements'] - ]); - - $operationName = $operation['name']; - } else { - $operationName = $options['operationName']; - } + $operation = $this->connection->createDatabase([ + 'instance' => $this->fullyQualifiedInstanceName(), + 'createStatement' => $statement, + 'extraStatements' => $options['statements'] + ]); - return $this->getOperation( - $this->lroConnection, - $operationName, - 'createDatabase', - function($result) use ($name) { - return $this->database($name); - } - ); + return $this->lro($operation['name']); } /** @@ -405,6 +371,7 @@ public function database($name) $this, $this->sessionPool, $this->lroConnection, + $this->lroCallables, $this->projectId, $name, $this->returnInt64AsObject diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 65e3830bdec0..4010f4d743f3 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -21,6 +21,7 @@ use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Int64; use Google\Cloud\LongRunning\LROTrait; +use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\Grpc; use Google\Cloud\Spanner\Connection\LongRunningConnection; @@ -129,6 +130,31 @@ public function __construct(array $config = []) $this->sessionPool = new SimpleSessionPool($this->sessionClient); $this->returnInt64AsObject = $config['returnInt64AsObject']; + + $this->lroCallables = [ + [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.UpdateInstanceMetadata', + 'callable' => function ($instance) { + $name = InstanceAdminClient::parseInstanceFromInstanceName($instance['name']); + return $this->instance($name, $instance); + } + ], [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.database.v1.CreateDatabaseMetadata', + 'callable' => function($database) { + $instanceName = DatabaseAdminClient::parseInstanceFromDatabaseName($database['name']); + $databaseName = DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']); + + $instance = $this->instance($instanceName); + return $instance->database($databaseName); + } + ], [ + 'typeUrl' => 'type.googleapis.com/google.spanner.admin.instance.v1.CreateInstanceMetadata', + 'callable' => function($instance) { + $name = InstanceAdminClient::parseInstanceFromInstanceName($instance['name']); + return $this->instance($name, $instance); + } + ] + ]; } /** @@ -215,9 +241,6 @@ public function configuration($name, array $config = []) * @type int $nodeCount **Defaults to** `1`. * @type array $labels For more information, see * [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). - * @type string $operationName If checking the status of an existing - * update operation, it may be supplied here. Note that if an - * operation name is given, no service requests will be executed. * } * @return LongRunningOperation * @codingStandardsIgnoreEnd @@ -234,27 +257,14 @@ public function createInstance(Configuration $config, $name, array $options = [] // This must always be set to CREATING, so overwrite anything else. $options['state'] = State::CREATING; - if (is_null($options['operationName'])) { - $operation = $this->connection->createInstance([ - 'instanceId' => $name, - 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), - 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), - 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) - ] + $options); - - $operationName = $operation['name']; - } else { - $operationName = $options['operationName']; - } + $operation = $this->connection->createInstance([ + 'instanceId' => $name, + 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), + 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) + ] + $options); - return $this->getOperation( - $this->lroConnection, - $operationName, - 'createInstance', - function($result) use ($name) { - return $this->instance($name, $result); - } - ); + return $this->lro($operation['name']); } /** @@ -274,6 +284,7 @@ public function instance($name, array $instance = []) $this->connection, $this->sessionPool, $this->lroConnection, + $this->lroCallables, $this->projectId, $name, $this->returnInt64AsObject, @@ -460,4 +471,15 @@ public function sessionClient() { return $this->sessionClient; } + + /** + * Resume a Long Running Operation + * + * @param string $operationName The Long Running Operation name. + * @return LongRunningOperation + */ + public function resumeOperation($operationName) + { + return $this->getOperation($this->lroConnection, $operationName); + } } From b3e6ba077f02df9a88467cd78d8d8cf731772f5c Mon Sep 17 00:00:00 2001 From: jdpedrie Date: Thu, 9 Feb 2017 11:43:28 -0500 Subject: [PATCH 056/107] Update documentation --- src/LongRunning/LROTrait.php | 9 + .../LongRunningConnectionInterface.php | 15 ++ src/LongRunning/LongRunningOperation.php | 158 +++++++++++++----- src/Spanner/Database.php | 10 ++ src/Spanner/Instance.php | 10 ++ src/Spanner/SpannerClient.php | 10 ++ 6 files changed, 173 insertions(+), 39 deletions(-) diff --git a/src/LongRunning/LROTrait.php b/src/LongRunning/LROTrait.php index c32d7270885f..2a456eb0fc6b 100644 --- a/src/LongRunning/LROTrait.php +++ b/src/LongRunning/LROTrait.php @@ -17,8 +17,17 @@ namespace Google\Cloud\LongRunning; +/** + * Provide Long Running Operation support to Google Cloud PHP Clients. + */ trait LROTrait { + /** + * Create a Long Running Operation from an operation name. + * + * @param string $operationName The name of the Operation. + * @return LongRunningOperation + */ public function lro($operationName) { return new LongRunningOperation( diff --git a/src/LongRunning/LongRunningConnectionInterface.php b/src/LongRunning/LongRunningConnectionInterface.php index ec41f48ce780..d6981dc69f0f 100644 --- a/src/LongRunning/LongRunningConnectionInterface.php +++ b/src/LongRunning/LongRunningConnectionInterface.php @@ -17,13 +17,28 @@ namespace Google\Cloud\LongRunning; +/** + * Defines the calls required to manage Long Running Operations + */ interface LongRunningConnectionInterface { + /** + * @param array $args + */ public function get(array $args); + /** + * @param array $args + */ public function cancel(array $args); + /** + * @param array $args + */ public function delete(array $args); + /** + * @param array $args + */ public function list(array $args); } diff --git a/src/LongRunning/LongRunningOperation.php b/src/LongRunning/LongRunningOperation.php index 58af7f70a9af..ae2b6b995453 100644 --- a/src/LongRunning/LongRunningOperation.php +++ b/src/LongRunning/LongRunningOperation.php @@ -17,10 +17,23 @@ namespace Google\Cloud\LongRunning; +/** + * Represent and interact with a Long Running Operation. + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * $instance = $spanner->instance('my-instance'); + * + * $operation = $instance->createDatabase('my-database'); + * ``` + */ class LongRunningOperation { - const MAX_RELOADS = 10; - const WAIT_INTERVAL = 1000; + const WAIT_INTERVAL = 1.0; const STATE_IN_PROGRESS = 'inProgress'; const STATE_SUCCESS = 'success'; @@ -73,6 +86,11 @@ public function __construct( /** * Return the Operation name. * + * Example: + * ``` + * $name = $operation->name(); + * ``` + * * @return string */ public function name() @@ -80,19 +98,16 @@ public function name() return $this->name; } - /** - * Return the Operation method. - * - * @return string - */ - public function method() - { - return $this->method; - } - /** * Check if the Operation is done. * + * Example: + * ``` + * if ($operation->done()) { + * echo "The operation is done!"; + * } + * ``` + * * @return bool */ public function done() @@ -109,6 +124,23 @@ public function done() * `LongRunningOperation::STATE_SUCCESS` or * `LongRunningOperation::STATE_ERROR`. * + * Example: + * ``` + * switch ($operation->state()) { + * case LongRunningOperation::STATE_IN_PROGRESS: + * echo "Operation is in progress"; + * break; + * + * case LongRunningOperation::STATE_SUCCESS: + * echo "Operation succeeded"; + * break; + * + * case LongRunningOperation::STATE_ERROR: + * echo "Operation failed"; + * break; + * } + * ``` + * * @return string */ public function state() @@ -131,6 +163,16 @@ public function state() * * Returns null if the Operation is not yet complete, or if an error occurred. * + * Note that if the Operation has not yet been reloaded, this may return + * null even when the operation has completed. Use + * {@see Google\Cloud\LongRunning\LongRunningOperation::reload()} to get the + * Operation state before retrieving the result. + * + * Example: + * ``` + * $result = $operation->result(); + * ``` + * * @return mixed|null */ public function result() @@ -143,6 +185,16 @@ public function result() * * Returns null if the Operation is not yet complete, or if no error occurred. * + * Note that if the Operation has not yet been reloaded, this may return + * null even when the operation has completed. Use + * {@see Google\Cloud\LongRunning\LongRunningOperation::reload()} to get the + * Operation state before retrieving the result. + * + * Example: + * ``` + * $error = $operation->error(); + * ``` + * * @return array|null */ public function error() @@ -153,6 +205,14 @@ public function error() /** * Get the Operation info. * + * If the Operation has not been checked previously, a service call will be + * executed. + * + * Example: + * ``` + * $info = $operation->info(); + * ``` + * * @codingStandardsIgnoreStart * @param array $options Configuration options. * @return array [google.longrunning.Operation](https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operation) @@ -166,6 +226,11 @@ public function info(array $options = []) /** * Reload the Operation to check its status. * + * Example: + * ``` + * $operation->reload(); + * ``` + * * @codingStandardsIgnoreStart * @param array $options Configuration Options. * @return array [google.longrunning.Operation](https://cloud.google.com/spanner/docs/reference/rpc/google.longrunning#google.longrunning.Operation) @@ -191,45 +256,41 @@ public function reload(array $options = []) /** * Reload the operation until it is complete. * - * The return type of this method is dictated by the type of Operation. + * The return type of this method is dictated by the type of Operation. If + * `$options.maxPollingDurationSeconds` is set, and the poll exceeds the + * limit, the return will be `null`. + * + * Example: + * ``` + * $result = $operation->pollUntilComplete(); + * ``` * * @param array $options { * Configuration Options * - * @type int $waitInterval The time, in microseconds, to wait between - * checking the status of the Operation. **Defaults to** `1000`. - * @type int $maxReloads The maximum number of reload operations the - * Operation will be checked. In microseconds, the time before - * failure will be `$waitInterval*$maxReloads`. **Defaults to** - * 10. + * @type float $pollingIntervalSeconds The polling interval to use, in + * seconds. **Defaults to** `1.0`. + * @type float $maxPollingDurationSeconds The maximum amount of time to + * continue polling. **Defaults to** `0.0`. * } - * @return mixed - * @throws RuntimeException If the max reloads are exceeded. + * @return mixed|null */ - public function wait(array $options = []) + public function pollUntilComplete(array $options = []) { $options += [ - 'waitInterval' => self::WAIT_INTERVAL, - 'maxReloads' => self::MAX_RELOADS + 'pollingIntervalSeconds' => $this::WAIT_INTERVAL, + 'maxPollingDurationSeconds' => 0.0, ]; - $isComplete = $this->done(); + $pollingIntervalMicros = $options['pollingIntervalSeconds'] * 1000000; + $maxPollingDuration = $options['maxPollingDurationSeconds']; + $hasMaxPollingDuration = $maxPollingDuration > 0.0; + $endTime = microtime(true) + $maxPollingDuration; - $reloads = 0; do { - $res = $this->reload($options); - $isComplete = $this->done(); - - if (!$isComplete) { - usleep($options['waitInterval']); - - $reloads++; - if ($reloads > $options['maxReloads']) { - throw new \RuntimeException('The maximum number of Operation reloads has been exceeded.'); - } - } - - } while(!$isComplete); + usleep($pollingIntervalMicros); + $this->reload($options); + } while (!$this->done() && (!$hasMaxPollingDuration || microtime(true) < $endTime)); return $this->result; } @@ -237,6 +298,11 @@ public function wait(array $options = []) /** * Cancel a Long Running Operation. * + * Example: + * ``` + * $operation->cancel(); + * ``` + * * @param array $options Configuration options. * @return void */ @@ -250,6 +316,11 @@ public function cancel(array $options = []) /** * Delete a Long Running Operation. * + * Example: + * ``` + * $operation->delete(); + * ``` + * * @param array $options Configuration Options. * @return void */ @@ -260,6 +331,14 @@ public function delete(array $options = []) ]); } + /** + * When the Operation is complete, there may be a callback enqueued to + * handle the response. If so, execute it and return the result. + * + * @param string $type The response type. + * @param mixed $response The response data. + * @return mixed + */ private function executeDoneCallback($type, $response) { if (is_null($response)) { @@ -288,6 +367,7 @@ public function __debugInfo() return [ 'connection' => get_class($this->connection), 'name' => $this->name, + 'callablesMap' => array_keys($this->callablesMap) ]; } } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 762e7ab840de..5d62cf979d2d 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -50,6 +50,16 @@ * $instance = $spanner->instance('my-instance'); * $database = $instance->database('my-database'); * ``` + * + * @method lro() { + * @param string $operationName The name of the Operation to resume. + * @return LongRunningOperation + * + * Example: + * ``` + * $operation = $database->lro($operationName); + * ``` + * } */ class Database { diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index ff6637a29427..618290699f52 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -40,6 +40,16 @@ * * $instance = $spanner->instance('my-instance'); * ``` + * + * @method lro() { + * @param string $operationName The name of the Operation to resume. + * @return LongRunningOperation + * + * Example: + * ``` + * $operation = $instance->lro($operationName); + * ``` + * } */ class Instance { diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 4010f4d743f3..5f736d2cc744 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -49,6 +49,16 @@ * * $spanner = new SpannerClient(); * ``` + * + * @method lro() { + * @param string $operationName The name of the Operation to resume. + * @return LongRunningOperation + * + * Example: + * ``` + * $operation = $spanner->lro($operationName); + * ``` + * } */ class SpannerClient { From fbc1e751f9a90a40836857f1e62e4eed514b2df2 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Wed, 15 Feb 2017 11:25:15 -0500 Subject: [PATCH 057/107] Spanner wip (#37) * Fix createInstance request * Fix Update DDL method * Make Result implement IteratorAggregate * Fix admin unit tests * Update docs and add updateDdlBatch method * Support disabling grpc key conversion * Add snippet and unit tests for all but Grpc connection * Grpc tests except for create/update instance * Add Grpc unit tests * Fix fixture * fixes * Address code review comments * Implement query parameterization and value encode/decode * Handle Arrays, Structs and NaN, INF, -INF correctly * Fix existing tests * Value Type Tests * Address code review * Finish covering the easy stuff * Cover ValueMapper * Support overloading any private property on tested class * More unit tests, refactor KeySet and KeyRange, fix Delete * Additional tests and miscellaneous improvements * Cover Transactions * Regenerate spanner * Include config changes and comment updates * Add operations tests * Update set connection calls * Fix spanner snippets * Update service endpoint * Updated Documentation * Update KeySet reference doc * Run transactions inside a callable * blah * Regenerate spanner with streaming methods * Add transaction ID to read, execute * Regenerate spanner with updated comments * Add snapshot, retry transactions, read in transaction * Remove interface, replace abstract class with trait * Support single use and begin transaction * Fix tests * Add unit test coverage * Additional unit test coverage * Update documentation * Fix doc --- composer.json | 9 +- dev/src/Functions.php | 21 + dev/src/StubTrait.php | 55 ++ src/ArrayTrait.php | 17 + src/Exception/AbortedException.php | 42 + .../Exception/FailedPreconditionException.php | 17 +- src/Exception/ServiceException.php | 14 +- src/GrpcRequestWrapper.php | 39 +- src/GrpcTrait.php | 20 + src/Logging/Connection/Grpc.php | 20 +- src/PhpArray.php | 24 +- src/PubSub/Connection/Grpc.php | 10 +- src/RequestWrapper.php | 4 + src/Retry.php | 106 +++ src/ServiceBuilder.php | 27 + .../Admin/Database/V1/DatabaseAdminClient.php | 246 ++++-- .../database_admin_client_config.json | 4 +- .../Admin/Instance/V1/InstanceAdminClient.php | 277 +++++-- .../instance_admin_client_config.json | 4 +- src/Spanner/Bytes.php | 112 +++ src/Spanner/Configuration.php | 31 +- .../Connection/AdminConnectionInterface.php | 111 --- src/Spanner/Connection/AdminGrpc.php | 263 ------- src/Spanner/Connection/Grpc.php | 236 ++++-- src/Spanner/Database.php | 741 +++++++++++++++--- src/Spanner/Date.php | 111 +++ src/Spanner/Duration.php | 122 +++ src/Spanner/Instance.php | 110 ++- src/Spanner/KeyRange.php | 209 ++++- src/Spanner/KeySet.php | 141 +++- src/Spanner/Operation.php | 273 +++++-- src/Spanner/Result.php | 175 +++-- src/Spanner/Session/Session.php | 11 + src/Spanner/Session/SessionClient.php | 19 +- src/Spanner/Session/SessionPoolInterface.php | 4 +- src/Spanner/Snapshot.php | 85 ++ src/Spanner/SpannerClient.php | 267 +++++-- src/Spanner/Timestamp.php | 100 +++ src/Spanner/Transaction.php | 386 ++++++--- src/Spanner/TransactionConfigurationTrait.php | 152 ++++ src/Spanner/TransactionReadTrait.php | 138 ++++ src/Spanner/V1/SpannerClient.php | 326 ++++++-- .../V1/resources/spanner_client_config.json | 4 +- src/Spanner/ValueInterface.php | 44 ++ src/Spanner/ValueMapper.php | 318 ++++++++ .../SpannerAdmin/ConfigurationTest.php | 119 +++ tests/snippets/SpannerAdmin/DatabaseTest.php | 147 ++++ tests/snippets/SpannerAdmin/InstanceTest.php | 215 +++++ .../SpannerAdmin/SpannerClientTest.php | 129 +++ tests/snippets/bootstrap.php | 2 +- tests/unit/ArrayTraitTest.php | 20 + tests/unit/PhpArrayTest.php | 2 +- tests/unit/Spanner/BytesTest.php | 52 ++ tests/unit/Spanner/DatabaseTest.php | 480 ++++++++++++ tests/unit/Spanner/DateTest.php | 55 ++ tests/unit/Spanner/DurationTest.php | 65 ++ tests/unit/Spanner/KeyRangeTest.php | 95 +++ tests/unit/Spanner/KeySetTest.php | 119 +++ tests/unit/Spanner/OperationTest.php | 303 +++++++ tests/unit/Spanner/ResultTest.php | 106 +++ tests/unit/Spanner/SnapshotTest.php | 48 ++ tests/unit/Spanner/SpannerClientTest.php | 112 +++ tests/unit/Spanner/TimestampTest.php | 61 ++ .../TransactionConfigurationTraitTest.php | 185 +++++ tests/unit/Spanner/TransactionTest.php | 331 ++++++++ tests/unit/Spanner/ValueMapperTest.php | 385 +++++++++ .../SpannerAdmin}/ConfigurationTest.php | 50 +- .../unit/SpannerAdmin/Connection/GrpcTest.php | 199 +++++ .../Connection/IamDatabaseTest.php | 24 +- .../Connection/IamInstanceTest.php | 24 +- .../SpannerAdmin}/DatabaseTest.php | 73 +- .../SpannerAdmin}/InstanceTest.php | 136 ++-- .../SpannerAdmin}/SpannerClientTest.php | 75 +- .../{ => unit}/fixtures/spanner/instance.json | 0 74 files changed, 7697 insertions(+), 1360 deletions(-) create mode 100644 dev/src/Functions.php create mode 100644 dev/src/StubTrait.php create mode 100644 src/Exception/AbortedException.php rename dev/src/SetStubConnectionTrait.php => src/Exception/FailedPreconditionException.php (60%) create mode 100644 src/Retry.php create mode 100644 src/Spanner/Bytes.php delete mode 100644 src/Spanner/Connection/AdminConnectionInterface.php delete mode 100644 src/Spanner/Connection/AdminGrpc.php create mode 100644 src/Spanner/Date.php create mode 100644 src/Spanner/Duration.php create mode 100644 src/Spanner/Snapshot.php create mode 100644 src/Spanner/Timestamp.php create mode 100644 src/Spanner/TransactionConfigurationTrait.php create mode 100644 src/Spanner/TransactionReadTrait.php create mode 100644 src/Spanner/ValueInterface.php create mode 100644 src/Spanner/ValueMapper.php create mode 100644 tests/snippets/SpannerAdmin/ConfigurationTest.php create mode 100644 tests/snippets/SpannerAdmin/DatabaseTest.php create mode 100644 tests/snippets/SpannerAdmin/InstanceTest.php create mode 100644 tests/snippets/SpannerAdmin/SpannerClientTest.php create mode 100644 tests/unit/Spanner/BytesTest.php create mode 100644 tests/unit/Spanner/DatabaseTest.php create mode 100644 tests/unit/Spanner/DateTest.php create mode 100644 tests/unit/Spanner/DurationTest.php create mode 100644 tests/unit/Spanner/KeyRangeTest.php create mode 100644 tests/unit/Spanner/KeySetTest.php create mode 100644 tests/unit/Spanner/OperationTest.php create mode 100644 tests/unit/Spanner/ResultTest.php create mode 100644 tests/unit/Spanner/SnapshotTest.php create mode 100644 tests/unit/Spanner/SpannerClientTest.php create mode 100644 tests/unit/Spanner/TimestampTest.php create mode 100644 tests/unit/Spanner/TransactionConfigurationTraitTest.php create mode 100644 tests/unit/Spanner/TransactionTest.php create mode 100644 tests/unit/Spanner/ValueMapperTest.php rename tests/{Spanner => unit/SpannerAdmin}/ConfigurationTest.php (63%) create mode 100644 tests/unit/SpannerAdmin/Connection/GrpcTest.php rename tests/{Spanner => unit/SpannerAdmin}/Connection/IamDatabaseTest.php (75%) rename tests/{Spanner => unit/SpannerAdmin}/Connection/IamInstanceTest.php (75%) rename tests/{Spanner => unit/SpannerAdmin}/DatabaseTest.php (62%) rename tests/{Spanner => unit/SpannerAdmin}/InstanceTest.php (64%) rename tests/{Spanner => unit/SpannerAdmin}/SpannerClientTest.php (67%) rename tests/{ => unit}/fixtures/spanner/instance.json (100%) diff --git a/composer.json b/composer.json index d9cbd5a50616..d80ce18249e2 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", "google/proto-client-php": "dev-master", - "google/gax": "^0.5" + "google/gax": "^0.7" }, "suggest": { "google/gax": "Required to support gRPC", @@ -72,7 +72,8 @@ "psr-4": { "Google\\Cloud\\Dev\\": "dev/src", "Google\\Cloud\\Tests\\System\\": "tests/system" - } + }, + "files": ["dev/src/Functions.php"] }, "scripts": { "google-cloud": "dev/google-cloud" @@ -81,6 +82,10 @@ { "type": "vcs", "url": "https://github.com/jdpedrie/proto-client-php-private" + }, + { + "type": "vcs", + "url": "https://github.com/michaelbausor/gax-php" } ] } diff --git a/dev/src/Functions.php b/dev/src/Functions.php new file mode 100644 index 000000000000..f38f42c391bf --- /dev/null +++ b/dev/src/Functions.php @@ -0,0 +1,21 @@ +newInstanceArgs($args); +} diff --git a/dev/src/StubTrait.php b/dev/src/StubTrait.php new file mode 100644 index 000000000000..7a710ed8bb72 --- /dev/null +++ b/dev/src/StubTrait.php @@ -0,0 +1,55 @@ +___getPropertyReflector($prop); + + $property->setAccessible(true); + return $property->getValue($this); + } + + public function ___setProperty($prop, $value) + { + if (!in_array($prop, json_decode($this->___props))) { + throw new \BadMethodCallException(sprintf('Property %s cannot be overloaded', $prop)); + } + + $property = $this->___getPropertyReflector($prop); + + $property->setAccessible(true); + $property->setValue($this, $value); + } + + private function ___getPropertyReflector($property) + { + $trait = new \ReflectionClass($this); + $ref = $trait->getParentClass(); + + try { + $property = $ref->getProperty($property); + } catch (\ReflectionException $e) { + throw new \BadMethodCallException($e->getMessage()); + } + + return $property; + } +} diff --git a/src/ArrayTrait.php b/src/ArrayTrait.php index 0bb5d43b1f96..78313b42f67a 100644 --- a/src/ArrayTrait.php +++ b/src/ArrayTrait.php @@ -78,4 +78,21 @@ private function isAssoc(array $arr) { return array_keys($arr) !== range(0, count($arr) - 1); } + + /** + * Just like array_filter(), but preserves falsey values except null. + * + * @param array $arr + * @return array + */ + private function arrayFilterRemoveNull(array $arr) + { + return array_filter($arr, function ($element) { + if (!is_null($element)) { + return true; + } + + return false; + }); + } } diff --git a/src/Exception/AbortedException.php b/src/Exception/AbortedException.php new file mode 100644 index 000000000000..593b6222a465 --- /dev/null +++ b/src/Exception/AbortedException.php @@ -0,0 +1,42 @@ +options, function ($metadataItem) { + if (array_key_exists('retryDelay', $metadataItem)) { + return true; + } + + return false; + }); + + $delay = $metadata[0]['retryDelay']; + if (!isset($delay['seconds'])) { + $delay['seconds'] = 0; + } + + return $delay; + } +} diff --git a/dev/src/SetStubConnectionTrait.php b/src/Exception/FailedPreconditionException.php similarity index 60% rename from dev/src/SetStubConnectionTrait.php rename to src/Exception/FailedPreconditionException.php index 3ffb3d5b606a..9f0d5b6dd1f9 100644 --- a/dev/src/SetStubConnectionTrait.php +++ b/src/Exception/FailedPreconditionException.php @@ -1,12 +1,12 @@ connection = $conn; - } + } diff --git a/src/Exception/ServiceException.php b/src/Exception/ServiceException.php index 87ae111eac19..eab79e38b58f 100644 --- a/src/Exception/ServiceException.php +++ b/src/Exception/ServiceException.php @@ -29,6 +29,11 @@ class ServiceException extends GoogleException */ private $serviceException; + /** + * @var array + */ + protected $options; + /** * Handle previous exceptions differently here. * @@ -36,9 +41,14 @@ class ServiceException extends GoogleException * @param int $code * @param Exception $serviceException */ - public function __construct($message, $code = null, Exception $serviceException = null) - { + public function __construct( + $message, + $code = null, + Exception $serviceException = null, + array $options = [] + ) { $this->serviceException = $serviceException; + $this->options = $options; parent::__construct($message, $code); } diff --git a/src/GrpcRequestWrapper.php b/src/GrpcRequestWrapper.php index a76ea808d574..5d2ffd9628ea 100644 --- a/src/GrpcRequestWrapper.php +++ b/src/GrpcRequestWrapper.php @@ -17,6 +17,7 @@ namespace Google\Cloud; +use DrSlump\Protobuf\Codec\Binary; use DrSlump\Protobuf\Codec\CodecInterface; use DrSlump\Protobuf\Message; use Google\Auth\FetchAuthTokenInterface; @@ -47,6 +48,11 @@ class GrpcRequestWrapper */ private $codec; + /** + * @var CodecInterface A codec used for binary deserialization. + */ + private $binaryCodec; + /** * @var array gRPC specific configuration options passed off to the GAX * library. @@ -63,6 +69,13 @@ class GrpcRequestWrapper Grpc\STATUS_DATA_LOSS ]; + /** + * @var array Map of error metadata types to RPC wrappers. + */ + private $metadataTypes = [ + 'google.rpc.retryinfo-bin' => \google\rpc\RetryInfo::class + ]; + /** * @param array $config [optional] { * Configuration options. Please see @@ -88,6 +101,7 @@ public function __construct(array $config = []) $this->authHttpHandler = $config['authHttpHandler'] ?: HttpHandlerFactory::build(); $this->codec = $config['codec']; $this->grpcOptions = $config['grpcOptions']; + $this->binaryCodec = new Binary; } /** @@ -127,7 +141,7 @@ public function send(callable $request, array $args, array $options = []) try { return $this->handleResponse($backoff->execute($request, $args)); - } catch (\Exception $ex) { + } catch (ApiException $ex) { throw $this->convertToGoogleException($ex); } } @@ -172,6 +186,10 @@ private function convertToGoogleException(ApiException $ex) $exception = Exception\ConflictException::class; break; + case Grpc\STATUS_FAILED_PRECONDITION: + $exception = Exception\FailedPreconditionException::class; + break; + case Grpc\STATUS_UNKNOWN: $exception = Exception\ServerException::class; break; @@ -180,11 +198,28 @@ private function convertToGoogleException(ApiException $ex) $exception = Exception\ServerException::class; break; + case Grpc\STATUS_ABORTED: + $exception = Exception\AbortedException::class; + break; + default: $exception = Exception\ServiceException::class; break; } - return new $exception($ex->getMessage(), $ex->getCode(), $ex); + $metadata = []; + if ($ex->getMetadata()) { + foreach ($ex->getMetadata() as $type => $binaryValue) { + if (!isset($this->metadataTypes[$type])) { + continue; + } + + $metadata[] = (new $this->metadataTypes[$type]) + ->deserialize($binaryValue[0], $this->binaryCodec) + ->serialize($this->codec); + } + } + + return new $exception($ex->getMessage(), $ex->getCode(), $ex, $metadata); } } diff --git a/src/GrpcTrait.php b/src/GrpcTrait.php index ffc0fcd6136e..65d6b2527b63 100644 --- a/src/GrpcTrait.php +++ b/src/GrpcTrait.php @@ -178,4 +178,24 @@ private function formatValueForApi($value) return ['list_value' => $this->formatListForApi($value)]; } } + + /** + * Format a timestamp for the API with nanosecond precision. + * + * @param string $value + * @return array + */ + private function formatTimestampForApi($value) + { + preg_match('/\.(\d{1,9})Z/', $value, $matches); + $value = preg_replace('/\.(\d{1,9})Z/', '.000000Z', $value); + + $dt = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.u\Z', $value); + $nanos = (isset($matches[1])) ? $matches[1] : 0; + + return [ + 'seconds' => (int)$dt->format('U'), + 'nanos' => (int)$nanos + ]; + } } diff --git a/src/Logging/Connection/Grpc.php b/src/Logging/Connection/Grpc.php index b9fc8f6d43dd..183b50beb1a9 100644 --- a/src/Logging/Connection/Grpc.php +++ b/src/Logging/Connection/Grpc.php @@ -89,15 +89,17 @@ class Grpc implements ConnectionInterface public function __construct(array $config = []) { $this->codec = new PhpArray([ - 'timestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - }, - 'severity' => function ($v) { - return Logger::getLogLevelMap()[$v]; - }, - 'outputVersionFormat' => function ($v) { - return self::$versionFormatMap[$v]; - } + 'customFilters' => [ + 'timestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + }, + 'severity' => function ($v) { + return Logger::getLogLevelMap()[$v]; + }, + 'outputVersionFormat' => function ($v) { + return self::$versionFormatMap[$v]; + } + ] ]); $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); diff --git a/src/PhpArray.php b/src/PhpArray.php index 984061bbbe3f..9fb1b215067c 100644 --- a/src/PhpArray.php +++ b/src/PhpArray.php @@ -33,12 +33,28 @@ class PhpArray extends Protobuf\Codec\PhpArray private $customFilters; /** - * @param array $customFilters A set of callbacks to apply to properties in + * @var bool + */ + private $useCamelCase; + + /** + * @param array $config [optional] { + * Configuration Options + * + * @type array $customFilters A set of callbacks to apply to properties in * a gRPC response. + * @type bool $useCamelCase Whether to convert key casing to camelCase. + * } */ - public function __construct(array $customFilters = []) + public function __construct(array $config = []) { - $this->customFilters = $customFilters; + $config += [ + 'useCamelCase' => true, + 'customFilters' => [] + ]; + + $this->customFilters = $config['customFilters']; + $this->useCamelCase = $config['useCamelCase']; } /** @@ -96,7 +112,7 @@ protected function encodeMessage(Protobuf\Message $message) $v = $this->filterValue($v, $field); } - $key = $this->toCamelCase($key); + $key = ($this->useCamelCase) ? $this->toCamelCase($key) : $key; if (isset($this->customFilters[$key])) { $v = call_user_func($this->customFilters[$key], $v); diff --git a/src/PubSub/Connection/Grpc.php b/src/PubSub/Connection/Grpc.php index 3731bfb88812..84b11c04a941 100644 --- a/src/PubSub/Connection/Grpc.php +++ b/src/PubSub/Connection/Grpc.php @@ -60,9 +60,13 @@ class Grpc implements ConnectionInterface */ public function __construct(array $config = []) { - $this->codec = new PhpArray(['publishTime' => function ($v) { - return $this->formatTimestampFromApi($v); - }]); + $this->codec = new PhpArray([ + 'customFilters' => [ + 'publishTime' => function ($v) { + return $this->formatTimestampFromApi($v); + } + ] + ]); $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); $grpcConfig = $this->getGaxConfig(); diff --git a/src/RequestWrapper.php b/src/RequestWrapper.php index 66d3e6579aae..ac76a6b4a269 100644 --- a/src/RequestWrapper.php +++ b/src/RequestWrapper.php @@ -207,6 +207,10 @@ private function convertToGoogleException(\Exception $ex) $exception = Exception\ConflictException::class; break; + case 412: + $exception = Exception\FailedPreconditionException::class; + break; + case 500: $exception = Exception\ServerException::class; break; diff --git a/src/Retry.php b/src/Retry.php new file mode 100644 index 000000000000..94167fb2b6fa --- /dev/null +++ b/src/Retry.php @@ -0,0 +1,106 @@ + (int >= 0), 'nanos' => (int >= 0)] specifying how + * long an operation should pause before retrying. Should accept a + * single argument of type `\Exception`. + * @param callable $retryFunction [optional] returns bool for whether or not + * to retry. + */ + public function __construct( + $retries, + callable $delayFunction, + callable $retryFunction = null + ) { + $this->retries = $retries !== null ? (int) $retries : 3; + $this->retryFunction = $retryFunction; + $this->delayFunction = $delayFunction; + } + + /** + * Executes the retry process. + * + * @param callable $function + * @param array $arguments [optional] + * @return mixed + * @throws \Exception The last exception caught while retrying. + */ + public function execute(callable $function, array $arguments = []) + { + $delayFunction = $this->delayFunction; + $retryAttempt = 0; + $exception = null; + + while (true) { + try { + return call_user_func_array($function, $arguments); + } catch (\Exception $exception) { + if ($this->retryFunction) { + if (!call_user_func($this->retryFunction, $exception)) { + throw $exception; + } + } + + if ($retryAttempt >= $this->retries) { + break; + } + + $delayFunction($exception); + $retryAttempt++; + } + } + + throw $exception; + } + + /** + * @param callable $delayFunction + * @return void + */ + public function setDelayFunction(callable $delayFunction) + { + $this->delayFunction = $delayFunction; + } +} diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index d8df7682795c..3e9b753f1cab 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -23,6 +23,7 @@ use Google\Cloud\Logging\LoggingClient; use Google\Cloud\NaturalLanguage\NaturalLanguageClient; use Google\Cloud\PubSub\PubSubClient; +use Google\Cloud\Spanner\SpannerClient; use Google\Cloud\Speech\SpeechClient; use Google\Cloud\Storage\StorageClient; use Google\Cloud\Translate\TranslateClient; @@ -113,6 +114,7 @@ public function __construct(array $config = []) * @type bool $returnInt64AsObject If true, 64 bit integers will be * returned as a {@see Google\Cloud\Int64} object for 32 bit * platform compatibility. **Defaults to** false. + * } * @return BigQueryClient */ public function bigQuery(array $config = []) @@ -209,6 +211,31 @@ public function pubsub(array $config = []) return new PubSubClient($config ? $this->resolveConfig($config) : $this->config); } + /** + * Google Cloud Spanner client. Google Cloud Spanner is a highly scalable, + * transactional, managed, NewSQL database service. Find more information + * at [Google Cloud Spanner API docs](https://cloud.google.com/spanner/). + * + * Example: + * ``` + * $spanner = $cloud->spanner(); + * ``` + * + * @param array $config [optional] { + * Configuration options. See + * {@see Google\Cloud\ServiceBuilder::__construct()} for the other available options. + * + * @type bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit + * platform compatibility. **Defaults to** false. + * } + * @return SpannerClient + */ + public function spanner(array $config = []) + { + return new SpannerClient($config ? $this->resolveConfig($config) : $this->config); + } + /** * Google Cloud Speech client. Enables easy integration of Google speech * recognition technologies into developer applications. Send audio and diff --git a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php index 2632057f67c6..332d10cc7728 100644 --- a/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php +++ b/src/Spanner/Admin/Database/V1/DatabaseAdminClient.php @@ -1,16 +1,18 @@ listDatabases($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $databaseAdminClient->close(); * } * ``` * @@ -83,7 +98,7 @@ class DatabaseAdminClient /** * The default address of the service. */ - const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + const SERVICE_ADDRESS = 'spanner.googleapis.com'; /** * The default port of the service. @@ -95,8 +110,15 @@ class DatabaseAdminClient */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _CODEGEN_NAME = 'gapic'; - const _CODEGEN_VERSION = '0.1.0'; + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; private static $instanceNameTemplate; private static $databaseNameTemplate; @@ -106,6 +128,7 @@ class DatabaseAdminClient private $scopes; private $defaultCallSettings; private $descriptors; + private $operationsClient; /** * Formats a string containing the fully-qualified path to represent @@ -212,6 +235,56 @@ private static function getPageStreamingDescriptors() return $pageStreamingDescriptors; } + private static function getLongRunningDescriptors() + { + return [ + 'createDatabase' => [ + 'operationReturnType' => '\google\spanner\admin\database\v1\Database', + 'metadataReturnType' => '\google\spanner\admin\database\v1\CreateDatabaseMetadata', + ], + 'updateDatabaseDdl' => [ + 'operationReturnType' => '\google\protobuf\EmptyC', + 'metadataReturnType' => '\google\spanner\admin\database\v1\UpdateDatabaseDdlMetadata', + ], + ]; + } + + /** + * Return an OperationsClient object with the same endpoint as $this. + * + * @return \Google\GAX\LongRunning\OperationsClient + */ + public function getOperationsClient() + { + return $this->operationsClient; + } + + /** + * Resume an existing long running operation that was previously started + * by a long running API method. If $methodName is not provided, or does + * not match a long running API method, then the operation can still be + * resumed, but the OperationResponse object will not deserialize the + * final response. + * + * @param string $operationName The name of the long running operation + * @param string $methodName The name of the method used to start the operation + * + * @return \Google\GAX\OperationResponse + */ + public function resumeOperation($operationName, $methodName = null) + { + $lroDescriptors = self::getLongRunningDescriptors(); + if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) { + $options = $lroDescriptors[$methodName]; + } else { + $options = []; + } + $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options); + $operation->reload(); + + return $operation; + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -220,14 +293,14 @@ private static function getPageStreamingDescriptors() * Optional. Options for configuring the service API wrapper. * * @type string $serviceAddress The domain name of the API remote host. - * Default 'wrenchworks.googleapis.com'. + * Default 'spanner.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. - * @type Grpc\ChannelCredentials $sslCreds + * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from - * Grpc\ChannelCredentials::createSsl() + * \Grpc\ChannelCredentials::createSsl() * @type array $scopes A string array of scopes to use when acquiring credentials. - * Default the scopes for the Google Cloud Spanner Admin Database API. + * Default the scopes for the Google Cloud Spanner Database Admin API. * @type array $retryingOverride * An associative array of string => RetryOptions, where the keys * are method names (e.g. 'createFoo'), that overrides default retrying @@ -240,21 +313,20 @@ private static function getPageStreamingDescriptors() * @type string $appName The codename of the calling service. Default 'gax'. * @type string $appVersion The version of the calling service. * Default: the current version of GAX. - * @type Google\Auth\CredentialsLoader $credentialsLoader + * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. * } */ public function __construct($options = []) { - $defaultScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.admin', - ]; $defaultOptions = [ 'serviceAddress' => self::SERVICE_ADDRESS, 'port' => self::DEFAULT_SERVICE_PORT, - 'scopes' => $defaultScopes, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', @@ -262,11 +334,20 @@ public function __construct($options = []) ]; $options = array_merge($defaultOptions, $options); + if (array_key_exists('operationsClient', $options)) { + $this->operationsClient = $options['operationsClient']; + } else { + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + ]); + } + $headerDescriptor = new AgentHeaderDescriptor([ 'clientName' => $options['appName'], 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::_CODEGEN_NAME, - 'codeGenVersion' => self::_CODEGEN_VERSION, + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -287,6 +368,10 @@ public function __construct($options = []) foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; } + $longRunningDescriptors = self::getLongRunningDescriptors(); + foreach ($longRunningDescriptors as $method => $longRunningDescriptor) { + $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient]; + } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/database_admin_client_config.json'); $clientConfig = json_decode($clientConfigJsonString, true); @@ -311,6 +396,9 @@ public function __construct($options = []) $createDatabaseAdminStubFunction = function ($hostname, $opts) { return new DatabaseAdminGrpcClient($hostname, $opts); }; + if (array_key_exists('createDatabaseAdminStubFunction', $options)) { + $createDatabaseAdminStubFunction = $options['createDatabaseAdminStubFunction']; + } $this->databaseAdminStub = $this->grpcCredentialsHelper->createStub( $createDatabaseAdminStubFunction, $options['serviceAddress'], @@ -327,13 +415,21 @@ public function __construct($options = []) * try { * $databaseAdminClient = new DatabaseAdminClient(); * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); - * foreach ($databaseAdminClient->listDatabases($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $databaseAdminClient->listDatabases($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $databaseAdminClient->close(); * } * ``` * @@ -406,11 +502,34 @@ public function listDatabases($parent, $optionalArgs = []) * $databaseAdminClient = new DatabaseAdminClient(); * $formattedParent = DatabaseAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $createStatement = ""; - * $response = $databaseAdminClient->createDatabase($formattedParent, $createStatement); - * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); + * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $databaseAdminClient->createDatabase($formattedParent, $createStatement); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'createDatabase'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) * } + * } finally { + * $databaseAdminClient->close(); * } * ``` * @@ -476,9 +595,7 @@ public function createDatabase($parent, $createStatement, $optionalArgs = []) * $formattedName = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $response = $databaseAdminClient->getDatabase($formattedName); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -535,11 +652,32 @@ public function getDatabase($name, $optionalArgs = []) * $databaseAdminClient = new DatabaseAdminClient(); * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $statements = []; - * $response = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); - * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); + * $operationResponse = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * // operation succeeded and returns no value + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $databaseAdminClient->updateDatabaseDdl($formattedDatabase, $statements); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $databaseAdminClient->resumeOperation($operationName, 'updateDatabaseDdl'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * // operation succeeded and returns no value + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) * } + * } finally { + * $databaseAdminClient->close(); * } * ``` * @@ -617,9 +755,7 @@ public function updateDatabaseDdl($database, $statements, $optionalArgs = []) * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $databaseAdminClient->dropDatabase($formattedDatabase); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -670,9 +806,7 @@ public function dropDatabase($database, $optionalArgs = []) * $formattedDatabase = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $response = $databaseAdminClient->getDatabaseDdl($formattedDatabase); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -728,9 +862,7 @@ public function getDatabaseDdl($database, $optionalArgs = []) * $policy = new Policy(); * $response = $databaseAdminClient->setIamPolicy($formattedResource, $policy); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -792,9 +924,7 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * $formattedResource = DatabaseAdminClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $response = $databaseAdminClient->getIamPolicy($formattedResource); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * @@ -853,9 +983,7 @@ public function getIamPolicy($resource, $optionalArgs = []) * $permissions = []; * $response = $databaseAdminClient->testIamPermissions($formattedResource, $permissions); * } finally { - * if (isset($databaseAdminClient)) { - * $databaseAdminClient->close(); - * } + * $databaseAdminClient->close(); * } * ``` * diff --git a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json index 16f75e93befb..efa919a0a7d8 100644 --- a/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json +++ b/src/Spanner/Admin/Database/V1/resources/database_admin_client_config.json @@ -12,9 +12,9 @@ }, "retry_params": { "default": { - "initial_retry_delay_millis": 100, + "initial_retry_delay_millis": 1000, "retry_delay_multiplier": 1.3, - "max_retry_delay_millis": 60000, + "max_retry_delay_millis": 32000, "initial_rpc_timeout_millis": 60000, "rpc_timeout_multiplier": 1.0, "max_rpc_timeout_millis": 60000, diff --git a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php index e60ef9b14714..bb7d0a10b257 100644 --- a/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php +++ b/src/Spanner/Admin/Instance/V1/InstanceAdminClient.php @@ -1,16 +1,18 @@ listInstanceConfigs($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -102,7 +116,7 @@ class InstanceAdminClient /** * The default address of the service. */ - const SERVICE_ADDRESS = 'wrenchworks.googleapis.com'; + const SERVICE_ADDRESS = 'spanner.googleapis.com'; /** * The default port of the service. @@ -114,8 +128,15 @@ class InstanceAdminClient */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _CODEGEN_NAME = 'gapic'; - const _CODEGEN_VERSION = '0.1.0'; + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; private static $projectNameTemplate; private static $instanceConfigNameTemplate; @@ -126,6 +147,7 @@ class InstanceAdminClient private $scopes; private $defaultCallSettings; private $descriptors; + private $operationsClient; /** * Formats a string containing the fully-qualified path to represent @@ -259,6 +281,56 @@ private static function getPageStreamingDescriptors() return $pageStreamingDescriptors; } + private static function getLongRunningDescriptors() + { + return [ + 'createInstance' => [ + 'operationReturnType' => '\google\spanner\admin\instance\v1\Instance', + 'metadataReturnType' => '\google\spanner\admin\instance\v1\CreateInstanceMetadata', + ], + 'updateInstance' => [ + 'operationReturnType' => '\google\spanner\admin\instance\v1\Instance', + 'metadataReturnType' => '\google\spanner\admin\instance\v1\UpdateInstanceMetadata', + ], + ]; + } + + /** + * Return an OperationsClient object with the same endpoint as $this. + * + * @return \Google\GAX\LongRunning\OperationsClient + */ + public function getOperationsClient() + { + return $this->operationsClient; + } + + /** + * Resume an existing long running operation that was previously started + * by a long running API method. If $methodName is not provided, or does + * not match a long running API method, then the operation can still be + * resumed, but the OperationResponse object will not deserialize the + * final response. + * + * @param string $operationName The name of the long running operation + * @param string $methodName The name of the method used to start the operation + * + * @return \Google\GAX\OperationResponse + */ + public function resumeOperation($operationName, $methodName = null) + { + $lroDescriptors = self::getLongRunningDescriptors(); + if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) { + $options = $lroDescriptors[$methodName]; + } else { + $options = []; + } + $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options); + $operation->reload(); + + return $operation; + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -267,14 +339,14 @@ private static function getPageStreamingDescriptors() * Optional. Options for configuring the service API wrapper. * * @type string $serviceAddress The domain name of the API remote host. - * Default 'wrenchworks.googleapis.com'. + * Default 'spanner.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. - * @type Grpc\ChannelCredentials $sslCreds + * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from - * Grpc\ChannelCredentials::createSsl() + * \Grpc\ChannelCredentials::createSsl() * @type array $scopes A string array of scopes to use when acquiring credentials. - * Default the scopes for the Google Cloud Spanner Admin Instance API. + * Default the scopes for the Google Cloud Spanner Instance Admin API. * @type array $retryingOverride * An associative array of string => RetryOptions, where the keys * are method names (e.g. 'createFoo'), that overrides default retrying @@ -287,21 +359,20 @@ private static function getPageStreamingDescriptors() * @type string $appName The codename of the calling service. Default 'gax'. * @type string $appVersion The version of the calling service. * Default: the current version of GAX. - * @type Google\Auth\CredentialsLoader $credentialsLoader + * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. * } */ public function __construct($options = []) { - $defaultScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.admin', - ]; $defaultOptions = [ 'serviceAddress' => self::SERVICE_ADDRESS, 'port' => self::DEFAULT_SERVICE_PORT, - 'scopes' => $defaultScopes, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.admin', + ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', @@ -309,11 +380,20 @@ public function __construct($options = []) ]; $options = array_merge($defaultOptions, $options); + if (array_key_exists('operationsClient', $options)) { + $this->operationsClient = $options['operationsClient']; + } else { + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + ]); + } + $headerDescriptor = new AgentHeaderDescriptor([ 'clientName' => $options['appName'], 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::_CODEGEN_NAME, - 'codeGenVersion' => self::_CODEGEN_VERSION, + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -335,6 +415,10 @@ public function __construct($options = []) foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; } + $longRunningDescriptors = self::getLongRunningDescriptors(); + foreach ($longRunningDescriptors as $method => $longRunningDescriptor) { + $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient]; + } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/instance_admin_client_config.json'); $clientConfig = json_decode($clientConfigJsonString, true); @@ -359,6 +443,9 @@ public function __construct($options = []) $createInstanceAdminStubFunction = function ($hostname, $opts) { return new InstanceAdminGrpcClient($hostname, $opts); }; + if (array_key_exists('createInstanceAdminStubFunction', $options)) { + $createInstanceAdminStubFunction = $options['createInstanceAdminStubFunction']; + } $this->instanceAdminStub = $this->grpcCredentialsHelper->createStub( $createInstanceAdminStubFunction, $options['serviceAddress'], @@ -375,13 +462,21 @@ public function __construct($options = []) * try { * $instanceAdminClient = new InstanceAdminClient(); * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); - * foreach ($instanceAdminClient->listInstanceConfigs($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstanceConfigs($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -449,9 +544,7 @@ public function listInstanceConfigs($parent, $optionalArgs = []) * $formattedName = InstanceAdminClient::formatInstanceConfigName("[PROJECT]", "[INSTANCE_CONFIG]"); * $response = $instanceAdminClient->getInstanceConfig($formattedName); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -501,13 +594,21 @@ public function getInstanceConfig($name, $optionalArgs = []) * try { * $instanceAdminClient = new InstanceAdminClient(); * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); - * foreach ($instanceAdminClient->listInstances($formattedParent) as $element) { - * // doThingsWith(element); + * // Iterate through all elements + * $pagedResponse = $instanceAdminClient->listInstances($formattedParent); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); * } - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $instanceAdminClient->listInstances($formattedParent, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -536,13 +637,15 @@ public function getInstanceConfig($name, $optionalArgs = []) * Some examples of using filters are: * * * name:* --> The instance has a name. - * * name:Howl --> The instance's name is howl. + * * 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's label env has the value dev. - * * name:howl labels.env:dev --> The instance's name is howl and it has - * the label env with value dev. + * * labels.env:* --> The instance has the label "env". + * * labels.env:dev --> The instance has the label "env" and the value of + * the label contains the string "dev". + * * name:howl labels.env:dev --> The instance's name contains "howl" and + * it has the label "env" with its value + * containing "dev". * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. @@ -595,9 +698,7 @@ public function listInstances($parent, $optionalArgs = []) * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $response = $instanceAdminClient->getInstance($formattedName); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -682,11 +783,34 @@ public function getInstance($name, $optionalArgs = []) * $formattedParent = InstanceAdminClient::formatProjectName("[PROJECT]"); * $instanceId = ""; * $instance = new Instance(); - * $response = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * $operationResponse = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $instanceAdminClient->createInstance($formattedParent, $instanceId, $instance); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'createInstance'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -783,11 +907,34 @@ public function createInstance($parent, $instanceId, $instance, $optionalArgs = * $instanceAdminClient = new InstanceAdminClient(); * $instance = new Instance(); * $fieldMask = new FieldMask(); - * $response = $instanceAdminClient->updateInstance($instance, $fieldMask); - * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); + * $operationResponse = $instanceAdminClient->updateInstance($instance, $fieldMask); + * $operationResponse->pollUntilComplete(); + * if ($operationResponse->operationSucceeded()) { + * $result = $operationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $operationResponse->getError(); + * // handleError($error) + * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $instanceAdminClient->updateInstance($instance, $fieldMask); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $instanceAdminClient->resumeOperation($operationName, 'updateInstance'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } + * } finally { + * $instanceAdminClient->close(); * } * ``` * @@ -854,9 +1001,7 @@ public function updateInstance($instance, $fieldMask, $optionalArgs = []) * $formattedName = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $instanceAdminClient->deleteInstance($formattedName); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -911,9 +1056,7 @@ public function deleteInstance($name, $optionalArgs = []) * $policy = new Policy(); * $response = $instanceAdminClient->setIamPolicy($formattedResource, $policy); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -975,9 +1118,7 @@ public function setIamPolicy($resource, $policy, $optionalArgs = []) * $formattedResource = InstanceAdminClient::formatInstanceName("[PROJECT]", "[INSTANCE]"); * $response = $instanceAdminClient->getIamPolicy($formattedResource); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * @@ -1036,9 +1177,7 @@ public function getIamPolicy($resource, $optionalArgs = []) * $permissions = []; * $response = $instanceAdminClient->testIamPermissions($formattedResource, $permissions); * } finally { - * if (isset($instanceAdminClient)) { - * $instanceAdminClient->close(); - * } + * $instanceAdminClient->close(); * } * ``` * diff --git a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json index 6771a7e9d440..23dbca4fe655 100644 --- a/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json +++ b/src/Spanner/Admin/Instance/V1/resources/instance_admin_client_config.json @@ -12,9 +12,9 @@ }, "retry_params": { "default": { - "initial_retry_delay_millis": 100, + "initial_retry_delay_millis": 1000, "retry_delay_multiplier": 1.3, - "max_retry_delay_millis": 60000, + "max_retry_delay_millis": 32000, "initial_rpc_timeout_millis": 60000, "rpc_timeout_multiplier": 1.0, "max_rpc_timeout_millis": 60000, diff --git a/src/Spanner/Bytes.php b/src/Spanner/Bytes.php new file mode 100644 index 000000000000..f0e3e4d6daf3 --- /dev/null +++ b/src/Spanner/Bytes.php @@ -0,0 +1,112 @@ +spanner(); + * + * $bytes = $spanner->bytes('hello world'); + * ``` + * + * ``` + * // Bytes objects can be cast to strings for easy display. + * echo (string) $bytes; + * ``` + */ +class Bytes implements ValueInterface +{ + /** + * @var string|resource|StreamInterface + */ + private $value; + + /** + * @param string|resource|StreamInterface $value The bytes value. + */ + public function __construct($value) + { + $this->value = Psr7\stream_for($value); + } + + /** + * Get the bytes as a stream. + * + * Example: + * ``` + * $stream = $bytes->get(); + * ``` + * + * @return StreamInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * Example: + * ``` + * echo $bytes->type(); + * ``` + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_BYTES; + } + + /** + * Format the value as a string. + * + * Example: + * ``` + * echo $bytes->formatAsString(); + * ``` + * + * @return string + */ + public function formatAsString() + { + return base64_encode((string) $this->value); + } + + /** + * Format the value as a string. + * + * @return string + * @access private + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Configuration.php b/src/Spanner/Configuration.php index 882f729a6180..983b1204a2c2 100644 --- a/src/Spanner/Configuration.php +++ b/src/Spanner/Configuration.php @@ -22,7 +22,7 @@ use Google\Cloud\Spanner\Connection\ConnectionInterface; /** - * Represents a Cloud Spanner Configuration + * Represents a Cloud Spanner Configuration. * * Example: * ``` @@ -33,6 +33,10 @@ * * $configuration = $spanner->configuration('regional-europe-west'); * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig InstanceConfig + * @codingStandardsIgnoreEnd */ class Configuration { @@ -98,14 +102,17 @@ public function name() * * This method may require a service call. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $info = $configuration->info(); - * echo $info['nodeCount']; * ``` * + * @codingStandardsIgnoreStart * @param array $options [optional] Configuration options. - * @return array + * @return array [InstanceConfig](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig) + * @codingStandardsIgnoreEnd */ public function info(array $options = []) { @@ -121,15 +128,17 @@ public function info(array $options = []) * * This method requires a service call. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * if ($configuration->exists()) { - * echo 'The configuration exists!'; + * echo 'Configuration exists!'; * } * ``` * * @param array $options [optional] Configuration options. - * @return array + * @return bool */ public function exists(array $options = []) { @@ -145,13 +154,17 @@ public function exists(array $options = []) /** * Fetch a fresh representation of the configuration from the service. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $info = $configuration->reload(); * ``` * + * @codingStandardsIgnoreStart * @param array $options [optional] Configuration options. - * @return array + * @return array [InstanceConfig](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#instanceconfig) + * @codingStandardsIgnoreEnd */ public function reload(array $options = []) { @@ -163,6 +176,12 @@ public function reload(array $options = []) return $this->info; } + /** + * A more readable representation of the object. + * + * @codeCoverageIgnore + * @access private + */ public function __debugInfo() { return [ diff --git a/src/Spanner/Connection/AdminConnectionInterface.php b/src/Spanner/Connection/AdminConnectionInterface.php deleted file mode 100644 index 8039c3244039..000000000000 --- a/src/Spanner/Connection/AdminConnectionInterface.php +++ /dev/null @@ -1,111 +0,0 @@ - CredentialsLoader::makeCredentials($config['scopes'], $config['keyFile']) - ]; - - $this->wrapper = new GrpcRequestWrapper; - - $this->instanceAdminClient = new InstanceAdminClient($grpcConfig); - $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); - } - - /** - * @param array $args [optional] - */ - public function listConfigs(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ - $args['projectId'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getConfig(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function listInstances(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'listInstances'], [ - InstanceAdminClient::formatProjectName($args['projectId']), - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getInstance(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'getInstance'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function createInstance(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'createInstance'], [ - $args['name'], - $args['config'], - $args['displayName'], - $args['nodeCount'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function updateInstance(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'updateInstance'], [ - $args['name'], - $args['config'], - $args['displayName'], - $args['nodeCount'], - new State, - $args['labels'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function deleteInstance(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getInstanceIamPolicy(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ - $args['resource'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function setInstanceIamPolicy(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ - $args['resource'], - $args['policy'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function testInstanceIamPermissions(array $args = []) - { - return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ - $args['resource'], - $args['permissions'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function listDatabases(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'listDatabases'], [ - $args['instance'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function createDatabase(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'createDatabase'], [ - $args['instance'], - $args['createStatement'], - $args['extraStatements'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function updateDatabase(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'updateDatabase'], [ - $args['name'], - $args['statements'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function dropDatabase(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getDatabaseDDL(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ - $args['name'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function getDatabaseIamPolicy(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ - $args['resource'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function setDatabaseIamPolicy(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ - $args['resource'], - $args['policy'], - $args - ]); - } - - /** - * @param array $args [optional] - */ - public function testDatabaseIamPermissions(array $args = []) - { - return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ - $args['resource'], - $args['permissions'], - $args - ]); - } -} diff --git a/src/Spanner/Connection/Grpc.php b/src/Spanner/Connection/Grpc.php index c5f27873f859..212e78ffc8a3 100644 --- a/src/Spanner/Connection/Grpc.php +++ b/src/Spanner/Connection/Grpc.php @@ -26,10 +26,14 @@ use Google\Cloud\Spanner\V1\SpannerClient; use Google\GAX\ApiException; use google\protobuf; +use google\spanner\admin\instance\v1\Instance; use google\spanner\admin\instance\v1\State; use google\spanner\v1; +use google\spanner\v1\KeySet; use google\spanner\v1\Mutation; use google\spanner\v1\TransactionOptions; +use google\spanner\v1\TransactionSelector; +use google\spanner\v1\Type; class Grpc implements ConnectionInterface { @@ -62,7 +66,8 @@ class Grpc implements ConnectionInterface 'insert' => 'setInsert', 'update' => 'setUpdate', 'upsert' => 'setInsertOrUpdate', - 'replace' => 'replace', + 'replace' => 'setReplace', + 'delete' => 'setDelete' ]; /** @@ -70,19 +75,21 @@ class Grpc implements ConnectionInterface */ public function __construct(array $config = []) { - $grpcConfig = [ - 'credentialsLoader' => CredentialsLoader::makeCredentials($config['scopes'], $config['keyFile']) - ]; - $this->codec = new PhpArray([ - 'timestamp' => function ($v) { - return $this->formatTimestampFromApi($v); - } + 'customFilters' => [ + 'commitTimestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + }, + 'readTimestamp' => function ($v) { + return $this->formatTimestampFromApi($v); + } + ] ]); $config['codec'] = $this->codec; $this->setRequestWrapper(new GrpcRequestWrapper($config)); + $grpcConfig = $this->getGaxConfig(); $this->instanceAdminClient = new InstanceAdminClient($grpcConfig); $this->databaseAdminClient = new DatabaseAdminClient($grpcConfig); $this->spannerClient = new SpannerClient($grpcConfig); @@ -94,7 +101,7 @@ public function __construct(array $config = []) public function listConfigs(array $args = []) { return $this->send([$this->instanceAdminClient, 'listInstanceConfigs'], [ - $args['projectId'], + $this->pluck('projectId', $args), $args ]); } @@ -105,7 +112,7 @@ public function listConfigs(array $args = []) public function getConfig(array $args = []) { return $this->send([$this->instanceAdminClient, 'getInstanceConfig'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -116,7 +123,7 @@ public function getConfig(array $args = []) public function listInstances(array $args = []) { return $this->send([$this->instanceAdminClient, 'listInstances'], [ - InstanceAdminClient::formatProjectName($args['projectId']), + $this->pluck('projectId', $args), $args ]); } @@ -127,7 +134,7 @@ public function listInstances(array $args = []) public function getInstance(array $args = []) { return $this->send([$this->instanceAdminClient, 'getInstance'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -137,11 +144,11 @@ public function getInstance(array $args = []) */ public function createInstance(array $args = []) { + $instance = $this->instanceObject($args, true); return $this->send([$this->instanceAdminClient, 'createInstance'], [ - $args['name'], - $args['config'], - $args['displayName'], - $args['nodeCount'], + $this->pluck('projectId', $args), + $this->pluck('instanceId', $args), + $instance, $args ]); } @@ -151,13 +158,15 @@ public function createInstance(array $args = []) */ public function updateInstance(array $args = []) { + $instanceObject = $this->instanceObject($args); + + $mask = array_keys($instanceObject->serialize(new PhpArray(['useCamelCase' => false]))); + + $fieldMask = (new protobuf\FieldMask())->deserialize(['paths' => $mask], $this->codec); + return $this->send([$this->instanceAdminClient, 'updateInstance'], [ - $args['name'], - $args['config'], - $args['displayName'], - $args['nodeCount'], - new State, - $args['labels'], + $instanceObject, + $fieldMask, $args ]); } @@ -168,7 +177,7 @@ public function updateInstance(array $args = []) public function deleteInstance(array $args = []) { return $this->send([$this->instanceAdminClient, 'deleteInstance'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -179,7 +188,7 @@ public function deleteInstance(array $args = []) public function getInstanceIamPolicy(array $args = []) { return $this->send([$this->instanceAdminClient, 'getIamPolicy'], [ - $args['resource'], + $this->pluck('resource', $args), $args ]); } @@ -190,8 +199,8 @@ public function getInstanceIamPolicy(array $args = []) public function setInstanceIamPolicy(array $args = []) { return $this->send([$this->instanceAdminClient, 'setIamPolicy'], [ - $args['resource'], - $args['policy'], + $this->pluck('resource', $args), + $this->pluck('policy', $args), $args ]); } @@ -202,8 +211,8 @@ public function setInstanceIamPolicy(array $args = []) public function testInstanceIamPermissions(array $args = []) { return $this->send([$this->instanceAdminClient, 'testIamPermissions'], [ - $args['resource'], - $args['permissions'], + $this->pluck('resource', $args), + $this->pluck('permissions', $args), $args ]); } @@ -214,7 +223,7 @@ public function testInstanceIamPermissions(array $args = []) public function listDatabases(array $args = []) { return $this->send([$this->databaseAdminClient, 'listDatabases'], [ - $args['instance'], + $this->pluck('instance', $args), $args ]); } @@ -225,9 +234,9 @@ public function listDatabases(array $args = []) public function createDatabase(array $args = []) { return $this->send([$this->databaseAdminClient, 'createDatabase'], [ - $args['instance'], - $args['createStatement'], - $args['extraStatements'], + $this->pluck('instance', $args), + $this->pluck('createStatement', $args), + $this->pluck('extraStatements', $args), $args ]); } @@ -237,9 +246,9 @@ public function createDatabase(array $args = []) */ public function updateDatabase(array $args = []) { - return $this->send([$this->databaseAdminClient, 'updateDatabase'], [ - $args['name'], - $args['statements'], + return $this->send([$this->databaseAdminClient, 'updateDatabaseDdl'], [ + $this->pluck('name', $args), + $this->pluck('statements', $args), $args ]); } @@ -250,7 +259,7 @@ public function updateDatabase(array $args = []) public function dropDatabase(array $args = []) { return $this->send([$this->databaseAdminClient, 'dropDatabase'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -261,7 +270,7 @@ public function dropDatabase(array $args = []) public function getDatabaseDDL(array $args = []) { return $this->send([$this->databaseAdminClient, 'getDatabaseDDL'], [ - $args['name'], + $this->pluck('name', $args), $args ]); } @@ -272,7 +281,7 @@ public function getDatabaseDDL(array $args = []) public function getDatabaseIamPolicy(array $args = []) { return $this->send([$this->databaseAdminClient, 'getIamPolicy'], [ - $args['resource'], + $this->pluck('resource', $args), $args ]); } @@ -283,8 +292,8 @@ public function getDatabaseIamPolicy(array $args = []) public function setDatabaseIamPolicy(array $args = []) { return $this->send([$this->databaseAdminClient, 'setIamPolicy'], [ - $args['resource'], - $args['policy'], + $this->pluck('resource', $args), + $this->pluck('policy', $args), $args ]); } @@ -295,8 +304,8 @@ public function setDatabaseIamPolicy(array $args = []) public function testDatabaseIamPermissions(array $args = []) { return $this->send([$this->databaseAdminClient, 'testIamPermissions'], [ - $args['resource'], - $args['permissions'], + $this->pluck('resource', $args), + $this->pluck('permissions', $args), $args ]); } @@ -339,8 +348,19 @@ public function deleteSession(array $args = []) */ public function executeSql(array $args = []) { - $args['params'] = (new protobuf\Struct) - ->deserialize($this->formatStructForApi($args['params']), $this->codec); + $params = new protobuf\Struct; + if (!empty($args['params'])) { + $params->deserialize($this->formatStructForApi($args['params']), $this->codec); + } + + $args['params'] = $params; + + foreach ($args['paramTypes'] as $key => $param) { + $args['paramTypes'][$key] = (new Type) + ->deserialize($param, $this->codec); + } + + $args['transaction'] = $this->createTransactionSelector($args); return $this->send([$this->spannerClient, 'executeSql'], [ $this->pluck('session', $args), @@ -354,36 +374,11 @@ public function executeSql(array $args = []) */ public function read(array $args = []) { - $keys = $this->pluck('keySet', $args); + $keySet = $this->pluck('keySet', $args); + $keySet = (new KeySet) + ->deserialize($this->formatKeySet($keySet), $this->codec); - $keySet = new v1\KeySet; - if (!empty($keys['keys'])) { - $keySet->setKeys($this->formatListForApi($keys['keys'])); - } - - if (!empty($keys['ranges'])) { - $ranges = new v1\KeyRange; - - if (isset($keys['ranges']['startClosed'])) { - $ranges->setStartClosed($this->formatListForApi($keys['ranges']['startClosed'])); - } - - if (isset($keys['ranges']['startOpen'])) { - $ranges->setStartOpen($this->formatListForApi($keys['ranges']['startOpen'])); - } - if (isset($keys['ranges']['endClosed'])) { - $ranges->setEndClosed($this->formatListForApi($keys['ranges']['endClosed'])); - } - if (isset($keys['ranges']['endOpen'])) { - $ranges->setEndOpen($this->formatListForApi($keys['ranges']['endOpen'])); - } - - $keySet->setRanges($ranges); - } - - if (isset($keys['all'])) { - $keySet->setAll($keys['all']); - } + $args['transaction'] = $this->createTransactionSelector($args); return $this->send([$this->spannerClient, 'read'], [ $this->pluck('session', $args), @@ -401,9 +396,19 @@ public function beginTransaction(array $args = []) { $options = new TransactionOptions; - if (isset($args['readOnly'])) { + if (isset($args['transactionOptions']['readOnly'])) { + $ro = $args['transactionOptions']['readOnly']; + + if (isset($ro['minReadTimestamp'])) { + $ro['minReadTimestamp'] = $this->formatTimestampForApi($ro['minReadTimestamp']); + } + + if (isset($ro['readTimestamp'])) { + $ro['readTimestamp'] = $this->formatTimestampForApi($ro['readTimestamp']); + } + $readOnly = (new TransactionOptions\ReadOnly) - ->deserialize($args['readOnly'], $this->codec); + ->deserialize($ro, $this->codec); $options->setReadOnly($readOnly); } else { @@ -430,36 +435,42 @@ public function commit(array $args = []) foreach ($inputMutations as $mutation) { $type = array_keys($mutation)[0]; $data = $mutation[$type]; - $data['values'] = $this->formatListForApi($data['values']); switch ($type) { case 'insert': case 'update': case 'upsert': case 'replace': - $write = (new Mutation\Write) - ->deserialize($data, $this->codec); + $data['values'] = $this->formatListForApi($data['values']); - $setterName = $this->mutationSetters[$type]; - $mutation = new Mutation; - $mutation->$setterName($write); - $mutations[] = $mutation; + $operation = (new Mutation\Write) + ->deserialize($data, $this->codec); break; case 'delete': - $mutations[] = (new Mutation\Delete) + if (isset($data['keySet'])) { + $data['keySet'] = $this->formatKeySet($data['keySet']); + } + + $operation = (new Mutation\Delete) ->deserialize($data, $this->codec); break; } + + $setterName = $this->mutationSetters[$type]; + $mutation = new Mutation; + $mutation->$setterName($operation); + $mutations[] = $mutation; } } if (isset($args['singleUseTransaction'])) { - $options = new TransactionOptions; $readWrite = (new TransactionOptions\ReadWrite) - ->deserialize($args['singleUseTransaction']['readWrite'], $this->codec); + ->deserialize([], $this->codec); + + $options = new TransactionOptions; $options->setReadWrite($readWrite); $args['singleUseTransaction'] = $options; } @@ -482,4 +493,59 @@ public function rollback(array $args = []) $args ]); } + + /** + * @param array $keySet + * @return array Formatted keyset + */ + private function formatKeySet(array $keySet) + { + if (isset($keySet['keys'])) { + $keySet['keys'] = $this->formatListForApi($keySet['keys']); + } + + if (isset($keySet['ranges'])) { + foreach ($keySet['ranges'] as $index => $rangeItem) { + foreach ($rangeItem as $key => $val) { + $rangeItem[$key] = $this->formatListForApi($val); + } + + $keySet['ranges'][$index] = $rangeItem; + } + } + + return $keySet; + } + + /** + * @param array $args + * @return array + */ + private function createTransactionSelector(array &$args) + { + $selector = new TransactionSelector; + if (isset($args['transaction'])) { + $selector = $selector->deserialize($this->pluck('transaction', $args), $this->codec); + } elseif (isset($args['transactionId'])) { + $selector = $selector->deserialize(['id' => $this->pluck('transactionId', $args)], $this->codec); + } + + return $selector; + } + + /** + * @param array $args + * @param bool $isRequired + */ + private function instanceObject(array &$args, $required = false) + { + return (new Instance())->deserialize(array_filter([ + 'name' => $this->pluck('name', $args, $required), + 'config' => $this->pluck('config', $args, $required), + 'displayName' => $this->pluck('displayName', $args, $required), + 'nodeCount' => $this->pluck('nodeCount', $args, $required), + 'state' => $this->pluck('state', $args, $required), + 'labels' => $this->formatLabelsForApi($this->pluck('labels', $args, $required)) + ]), $this->codec); + } } diff --git a/src/Spanner/Database.php b/src/Spanner/Database.php index 403636dd4eae..22eecdf79b45 100644 --- a/src/Spanner/Database.php +++ b/src/Spanner/Database.php @@ -17,23 +17,46 @@ namespace Google\Cloud\Spanner; +use Google\Cloud\ArrayTrait; +use Google\Cloud\Exception\AbortedException; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; -use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; +use Google\Cloud\Retry; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Google\Cloud\Spanner\Session\SessionPoolInterface; +use Google\Cloud\Spanner\V1\SpannerClient as GrpcSpannerClient; /** * Represents a Google Cloud Spanner Database * * Example: * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $database = $spanner->connect('my-instance', 'my-database'); + * ``` + * + * ``` + * // Databases can also be connected to via an Instance. + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $instance = $spanner->instance('my-instance'); * $database = $instance->database('my-database'); * ``` */ class Database { + use TransactionConfigurationTrait; + + const MAX_RETRIES = 3; + /** * @var ConnectionInterface */ @@ -78,14 +101,17 @@ class Database * @param SessionPoolInterface The session pool implementation. * @param string $projectId The project ID. * @param string $name The database name. - * @param array $info [optional] A representation of the database object. + * @param bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit platform + * compatibility. **Defaults to** false. */ public function __construct( ConnectionInterface $connection, Instance $instance, SessionPoolInterface $sessionPool, $projectId, - $name + $name, + $returnInt64AsObject = false ) { $this->connection = $connection; $this->instance = $instance; @@ -93,7 +119,7 @@ public function __construct( $this->projectId = $projectId; $this->name = $name; - $this->operation = new Operation($connection, $instance, $this); + $this->operation = new Operation($connection, $returnInt64AsObject); $this->iam = new Iam( new IamDatabase($this->connection), $this->fullyQualifiedDatabaseName() @@ -120,10 +146,12 @@ public function name() * * This method sends a service request. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * if ($database->exists()) { - * echo 'The database exists!'; + * echo 'Database exists!'; * } * ``` * @@ -133,9 +161,7 @@ public function name() public function exists(array $options = []) { try { - $this->connection->getDatabaseDDL($options + [ - 'name' => $this->fullyQualifiedDatabaseName() - ]); + $this->ddl($options); } catch (NotFoundException $e) { return false; } @@ -144,33 +170,71 @@ public function exists(array $options = []) } /** - * Update the Database. + * Update the Database schema by running a SQL statement. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. * * Example: * ``` - * $database->update([ + * $database->updateDdl( * 'CREATE TABLE Users ( * id INT64 NOT NULL, * name STRING(100) NOT NULL * password STRING(100) NOT NULL * )' + * ); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.UpdateDatabaseDdlRequest UpdateDDLRequest + * @codingStandardsIgnoreEnd + * + * @param string $statement A DDL statement to run against a database. + * @param array $options [optional] Configuration options. + * @return + */ + public function updateDdl($statement, array $options = []) + { + return $this->updateDdlBatch([$statement], $options); + } + + /** + * Update the Database schema by running a set of SQL statements. + * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * + * Example: + * ``` + * $database->updateDdlBatch([ + * 'CREATE TABLE Users ( + * id INT64 NOT NULL, + * name STRING(100) NOT NULL, + * password STRING(100) NOT NULL + * ) PRIMARY KEY (id)', + * 'CREATE TABLE Posts ( + * id INT64 NOT NULL, + * title STRING(100) NOT NULL, + * content STRING(MAX) NOT NULL + * ) PRIMARY KEY(id)' * ]); * ``` * - * @param string|array $statements One or more DDL statements to execute. + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/data-definition-language Data Definition Language + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.UpdateDatabaseDdlRequest UpdateDDLRequest + * @codingStandardsIgnoreEnd + * + * @param string[] $statements A list of DDL statements to run against a database. * @param array $options [optional] Configuration options. * @return */ - public function updateDdl($statements, array $options = []) + public function updateDdlBatch(array $statements, array $options = []) { $options += [ 'operationId' => null ]; - if (!is_array($statements)) { - $statements = [$statements]; - } - return $this->connection->updateDatabase($options + [ 'name' => $this->fullyQualifiedDatabaseName(), 'statements' => $statements, @@ -180,17 +244,23 @@ public function updateDdl($statements, array $options = []) /** * Drop the database. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $database->drop(); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#google.spanner.admin.database.v1.DropDatabaseRequest DropDatabaseRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration options. * @return void */ public function drop(array $options = []) { - return $this->connection->dropDatabase($options + [ + $this->connection->dropDatabase($options + [ 'name' => $this->fullyQualifiedDatabaseName() ]); } @@ -198,11 +268,17 @@ public function drop(array $options = []) /** * Get a list of all database DDL statements. * + * **NOTE**: Requires `https://www.googleapis.com/auth/spanner.admin` scope. + * * Example: * ``` * $statements = $database->ddl(); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#getdatabaseddlrequest GetDatabaseDdlRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration options. * @return array */ @@ -235,52 +311,241 @@ public function iam() } /** - * Create a Read Only transaction + * Create a snapshot to read from a database at a point in time. + * + * If no configuration options are provided, transaction will be opened with + * strong consistency. + * + * Snapshots are executed behind the scenes using a Read-Only Transaction. + * + * Example: + * ``` + * $snapshot = $database->snapshot(); + * ``` + * + * ``` + * // Take a shapshot with a returned timestamp. + * $snapshot = $database->snapshot([ + * 'returnReadTimestamp' => true + * ]); + * + * $timestamp = $snapshot->readTimestamp(); + * ``` * * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions + * * @param array $options [optional] { * Configuration Options * - * @type array $transactionOptions [TransactionOptions](https://cloud.google.com/spanner/reference/rest/v1/TransactionOptions). + * See [ReadOnly](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions.ReadOnly) + * for detailed description of available options. + * + * Please note that only one of `$strong`, `$readTimestamp` or + * `$exactStaleness` may be set in a request. + * + * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected + * read timestamp is included in the Transaction message that + * describes the transaction. + * @type bool $strong Read at a timestamp where all previously committed + * transactions are visible. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type Duration $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. * } + * @return Snapshot * @codingStandardsIgnoreEnd - * @return Transaction */ - public function readOnlyTransaction(array $options = []) + public function snapshot(array $options = []) + { + // These are only available in single-use transactions. + if (isset($options['maxStaleness']) || isset($options['minReadTimestamp'])) { + throw new \BadMethodCallException( + 'maxStaleness and minReadTimestamp are only available in single-use transactions.' + ); + } + + $transactionOptions = $this->configureSnapshotOptions($options); + + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + + return $this->operation->snapshot($session, $transactionOptions); + } + + /** + * Execute Read/Write operations inside a Transaction. + * + * Using this method and providing a callable operation provides certain + * benefits including automatic retry when a transaction fails. In case of a + * failure, all transaction operations, including reads, are re-applied in a + * new transaction. + * + * If a transaction exceeds the maximum number of retries, + * {@see Google\Cloud\Exception\AbortedException} will be thrown. Any other + * exception types will immediately bubble up and will interrupt the retry + * operation. + * + * Please note that once a transaction reads data, it will lock the read + * data, preventing other users from modifying that data. For this reason, + * it is important that every transaction commits or rolls back as early as + * possible. Do not hold transactions open longer than necessary. + * + * If you have an active transaction which was obtained from elsewhere, you + * can provide it to this method and gain the benefits of managed retry by + * setting `$options.transaction` to your {@see Google\Cloud\Spanner\Transaction} + * instance. Please note that in this case, it is important that ALL reads + * and mutations MUST be performed within the runTransaction callable. + * + * Example: + * ``` + * $transaction = $database->runTransaction(function (Transaction $t) use ($userName, $password) { + * $user = $t->execute('SELECT * FROM Users WHERE Name = @name and PasswordHash = @password', [ + * 'parameters' => [ + * 'name' => $userName, + * 'password' => password_hash($password) + * ] + * ])->firstRow(); + * + * if ($user) { + * grantAccess($user); + * + * $user['loginCount'] = $user['loginCount'] + 1; + * $t->update('Users', $user); + * } else { + * $t->rollback(); + * } + * + * $t->commit(); + * }); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions + * @codingStandardsIgnoreEnd + * + * @param callable $operation The operations to run in the transaction. + * **Signature:** `function (Transaction $transaction)`. + * @param array $options [optional] { + * Configuration Options + * + * @type int $maxRetries The number of times to attempt to apply the + * operation before failing. **Defaults to ** `3`. + * @type Transaction $transaction If provided, the transaction will be + * passed to the callable instead of attempting to begin a new + * transaction. + * } + * @return mixed The return value of `$operation`. + */ + public function runTransaction(callable $operation, array $options = []) { $options += [ - 'transactionOptions' => [] + 'maxRetries' => self::MAX_RETRIES, + 'transaction' => null ]; - if (empty($options['transactionOptions'])) { - $options['transactionOptions']['strong'] = true; - } + // There isn't anything configurable here. + $options['transactionOptions'] = $this->configureTransactionOptions(); - $options['readOnly'] = $options['transactionOptions']; + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + + $attempt = 0; + $startTransactionFn = function ($session, $options) use ($options, &$attempt) { + if ($attempt === 0 && $options['transaction'] instanceof Transaction) { + $transaction = $options['transaction']; + } else { + $transaction = $this->operation->transaction($session, $options); + } + + $attempt++; + return $transaction; + }; + + $delayFn = function (\Exception $e) { + if (!($e instanceof AbortedException)) { + throw $e; + } + + $delay = $e->getRetryDelay(); + time_nanosleep($delay['seconds'], $delay['nanos']); + }; - return $this->transaction(SessionPoolInterface::CONTEXT_READ, $options); + $commitFn = function($operation, $session, $options) use ($startTransactionFn) { + $transaction = call_user_func_array($startTransactionFn, [ + $session, + $options + ]); + + return call_user_func($operation, $transaction); + }; + + $retry = new Retry($options['maxRetries'], $delayFn); + return $retry->execute($commitFn, [$operation, $session, $options]); } /** - * Create a Read/Write transaction + * Create and return a new read/write Transaction. + * + * When manually using a Transaction, it is advised that retry logic be + * implemented to reapply all operations when an instance of + * {@see Google\Cloud\Exception\AbortedException} is thrown. + * + * If you wish Google Cloud PHP to handle retry logic for you (recommended + * for most cases), use {@see Google\Cloud\Spanner\Database::runTransaction()}. + * + * Please note that once a transaction reads data, it will lock the read + * data, preventing other users from modifying that data. For this reason, + * it is important that every transaction commits or rolls back as early as + * possible. Do not hold transactions open longer than necessary. + * + * Example: + * ``` + * $transaction = $database->transaction(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * @see https://cloud.google.com/spanner/docs/transactions Transactions + * @codingStandardsIgnoreEnd * - * @param array $options [optional] Configuration Options + * @param array $options [optional] Configuration Options. * @return Transaction */ - public function lockingTransaction(array $options = []) + public function transaction(array $options = []) { - $options['readWrite'] = []; + // There isn't anything configurable here. + $options['transactionOptions'] = [ + 'readWrite' => [] + ]; - return $this->transaction(SessionPoolInterface::CONTEXT_READWRITE, $options); + $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + return $this->operation->transaction($session, $options); } /** * Insert a row. * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->insert('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Welcome to our site.' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $data The row data to insert. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function insert($table, array $data, array $options = []) { @@ -290,10 +555,31 @@ public function insert($table, array $data, array $options = []) /** * Insert multiple rows. * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->insert('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Welcome to our site.' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'Lots of people ask about where we got started.' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $dataSet The row data to insert. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function insertBatch($table, array $dataSet, array $options = []) { @@ -304,16 +590,35 @@ public function insertBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } /** * Update a row. * + * Only data which you wish to update need be included. You must provide + * enough information for the API to determine which row should be modified. + * In most cases, this means providing values for the Primary Key fields. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->update('Posts', [ + * 'ID' => 1337, + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $data The row data to update. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function update($table, array $data, array $options = []) { @@ -323,10 +628,33 @@ public function update($table, array $data, array $options = []) /** * Update multiple rows. * + * Only data which you wish to update need be included. You must provide + * enough information for the API to determine which row should be modified. + * In most cases, this means providing values for the Primary Key fields. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->update('Posts', [ + * [ + * 'ID' => 1337, + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $dataSet The row data to update. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function updateBatch($table, array $dataSet, array $options = []) { @@ -337,16 +665,36 @@ public function updateBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } /** * Insert or update a row. * + * If a row already exists (determined by comparing the Primary Key to + * existing table data), the row will be updated. If not, it will be + * created. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->insertOrUpdate('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $data The row data to insert or update. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function insertOrUpdate($table, array $data, array $options = []) { @@ -356,10 +704,35 @@ public function insertOrUpdate($table, array $data, array $options = []) /** * Insert or update multiple rows. * + * If a row already exists (determined by comparing the Primary Key to + * existing table data), the row will be updated. If not, it will be + * created. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->insertOrUpdateBatch('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $dataSet The row data to insert or update. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) { @@ -370,16 +743,36 @@ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } /** * Replace a row. * + * Provide data for the entire row. Google Cloud Spanner will attempt to + * find a record matching the Primary Key, and will replace the entire row. + * If a matching row is not found, it will be inserted. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->replace('Posts', [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $data The row data to replace. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function replace($table, array $data, array $options = []) { @@ -389,10 +782,35 @@ public function replace($table, array $data, array $options = []) /** * Replace multiple rows. * + * Provide data for the entire row. Google Cloud Spanner will attempt to + * find a record matching the Primary Key, and will replace the entire row. + * If a matching row is not found, it will be inserted. + * + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $database->replaceBatch('Posts', [ + * [ + * 'ID' => 1337, + * 'postTitle' => 'Hello World!', + * 'postContent' => 'Thanks for visiting our site!' + * ], [ + * 'ID' => 1338, + * 'postTitle' => 'Our History', + * 'postContent' => 'A little bit about us!' + * ] + * ]); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd + * * @param string $table The table to mutate. * @param array $dataSet The row data to replace. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ public function replaceBatch($table, array $dataSet, array $options = []) { @@ -403,53 +821,136 @@ public function replaceBatch($table, array $dataSet, array $options = []) $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } /** - * Delete a row. + * Delete one or more rows. * - * @param string $table The table to mutate. - * @param array $key The key to use to identify the row or rows to delete. - * @param array $options [optional] Configuration options. - * @return array - */ - public function delete($table, array $key, array $options = []) - { - return $this->deleteBatch($table, [$key], $options); - } - - /** - * Delete multiple rows. + * Mutations are committed in a single-use transaction. + * + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [ + * 1337, 1338 + * ] + * ]); + * + * $database->delete('Posts', $keySet); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitRequest CommitRequest + * @codingStandardsIgnoreEnd * * @param string $table The table to mutate. - * @param array $keySets The keys to use to identify the row or rows to delete. + * @param KeySet $keySet The KeySet to identify rows to delete. * @param array $options [optional] Configuration options. - * @return array + * @return Timestamp The commit Timestamp. */ - public function deleteBatch($table, array $keySets, array $options = []) + public function delete($table, KeySet $keySet, array $options = []) { - $mutations = []; - foreach ($keySets as $keySet) { - $mutations[] = $this->operation->deleteMutation($table, $keySet); - } + $mutations = [$this->operation->deleteMutation($table, $keySet)]; $session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE); + $options['singleUseTransaction'] = $this->configureTransactionOptions(); return $this->operation->commit($session, $mutations, $options); } /** * Run a query. * + * Example: + * ``` + * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 + * ] + * ]); + * ``` + * + * ``` + * // Execute a read and return a new Snapshot for further reads. + * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 + * ], + * 'begin' => true + * ]); + * + * $snapshot = $result->snapshot(); + * ``` + * + * ``` + * // Execute a read and return a new Transaction for further reads and writes. + * $result = $spanner->execute('SELECT * FROM Posts WHERE ID = @postId', [ + * 'parameters' => [ + * 'postId' => 1337 + * ], + * 'begin' => true, + * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE + * ]); + * + * $transaction = $result->transaction(); + * ``` + * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ExecuteSqlRequest ExecuteSqlRequest + * @codingStandardsIgnoreEnd + * + * @codingStandardsIgnoreStart * @param string $sql The query string to execute. - * @param array $options [optional] Configuration options. + * @param array $options [optional] { + * Configuration Options. + * + * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions) + * for detailed description of available transaction options. + * + * Please note that only one of `$strong`, `$minReadTimestamp`, + * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in + * a request. + * + * @type array $parameters A key/value array of Query Parameters, where + * the key is represented in the query string prefixed by a `@` + * symbol. + * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected + * read timestamp is included in the Transaction message that + * describes the transaction. + * @type bool $strong Read at a timestamp where all previously committed + * transactions are visible. + * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the + * given timestamp. Only available in single-use transactions. + * @type Duration $maxStaleness Read data at a timestamp >= NOW - the + * given timestamp. Only available in single-use transactions. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type Duration $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. + * @type bool $begin If true, will begin a new transaction. If a + * read/write transaction is desired, set the value of + * $transactionType. If a transaction or snapshot is created, it + * will be returned as `$result->transaction()` or + * `$result->snapshot()`. **Defaults to** `false`. + * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ` + * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is + * chosen, any snapshot options will be disregarded. If `$begin` + * is false, this option will be ignored. **Defaults to** + * `SessionPoolInterface::CONTEXT_READ`. + * } + * @codingStandardsIgnoreEnd * @return Result */ public function execute($sql, array $options = []) { $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; + $options['transactionContext'] = $context; + return $this->operation->execute($session, $sql, $options); } @@ -459,48 +960,103 @@ public function execute($sql, array $options = []) * Note that if no KeySet is specified, all rows in a table will be * returned. * - * @todo is returning everything a reasonable default? + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [1337] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns); + * ``` * + * ``` + * // Execute a read and return a new Snapshot for further reads. + * $keySet = $spanner->keySet([ + * 'keys' => [1337] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns, [ + * 'begin' => true + * ]); + * + * $snapshot = $result->snapshot(); + * ``` + * + * ``` + * // Execute a read and return a new Transaction for further reads and writes. + * $keySet = $spanner->keySet([ + * 'keys' => [1337] + * ]); + * + * $columns = ['ID', 'title', 'content']; + * + * $result = $database->read('Posts', $keySet, $columns, [ + * 'begin' => true, + * 'transactionType' => SessionPoolInterface::CONTEXT_READWRITE + * ]); + * + * $transaction = $result->transaction(); + * ``` + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ReadRequest ReadRequest + * + * @codingStandardsIgnoreStart * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. * @param array $options [optional] { * Configuration Options. * + * See [TransactionOptions](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.TransactionOptions) + * for detailed description of available transaction options. + * + * Please note that only one of `$strong`, `$minReadTimestamp`, + * `$maxStaleness`, `$readTimestamp` or `$exactStaleness` may be set in + * a request. + * * @type string $index The name of an index on the table. - * @type array $columns A list of column names to be returned. - * @type array $keySet A [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). * @type int $offset The number of rows to offset results by. * @type int $limit The number of results to return. + * @type bool $returnReadTimestamp If true, the Cloud Spanner-selected + * read timestamp is included in the Transaction message that + * describes the transaction. + * @type bool $strong Read at a timestamp where all previously committed + * transactions are visible. + * @type Timestamp $minReadTimestamp Execute reads at a timestamp >= the + * given timestamp. Only available in single-use transactions. + * @type Duration $maxStaleness Read data at a timestamp >= NOW - the + * given timestamp. Only available in single-use transactions. + * @type Timestamp $readTimestamp Executes all reads at the given + * timestamp. + * @type Duration $exactStaleness Represents a number of seconds. Executes + * all reads at a timestamp that is $exactStaleness old. + * @type bool $begin If true, will begin a new transaction. If a + * read/write transaction is desired, set the value of + * $transactionType. If a transaction or snapshot is created, it + * will be returned as `$result->transaction()` or + * `$result->snapshot()`. **Defaults to** `false`. + * @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ` + * or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is + * chosen, any snapshot options will be disregarded. If `$begin` + * is false, this option will be ignored. **Defaults to** + * `SessionPoolInterface::CONTEXT_READ`. * } + * @codingStandardsIgnoreEnd + * @return Result */ - public function read($table, array $options = []) + public function read($table, KeySet $keySet, array $columns, array $options = []) { $session = $this->selectSession(SessionPoolInterface::CONTEXT_READ); - return $this->operation->read($session, $table, $options); - } - - /** - * Create a transaction with a given context. - * - * @param string $context The context of the new transaction. - * @param array $options [optional] Configuration options. - * @return Transaction - */ - private function transaction($context, array $options = []) - { - $options += [ - 'transactionOptions' => [] - ]; - - $session = $this->selectSession($context); - - // make a service call here. - $res = $this->connection->beginTransaction($options + [ - 'session' => $session->name(), - 'context' => $context, - ]); + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; + $options['transactionContext'] = $context; - return new Transaction($this->operation, $session, $context, $res); + return $this->operation->read($session, $table, $keySet, $columns, $options); } /** @@ -524,7 +1080,7 @@ private function selectSession($context = SessionPoolInterface::CONTEXT_READ) { */ private function fullyQualifiedDatabaseName() { - return DatabaseAdminClient::formatDatabaseName( + return GrpcSpannerClient::formatDatabaseName( $this->projectId, $this->instance->name(), $this->name @@ -542,7 +1098,10 @@ public function __debugInfo() return [ 'connection' => get_class($this->connection), 'projectId' => $this->projectId, - 'name' => $this->name + 'name' => $this->name, + 'instance' => $this->instance, + 'sessionPool' => $this->sessionPool, + 'returnInt64AsObject' => $this->returnInt64AsObject, ]; } } diff --git a/src/Spanner/Date.php b/src/Spanner/Date.php new file mode 100644 index 000000000000..777f18b48840 --- /dev/null +++ b/src/Spanner/Date.php @@ -0,0 +1,111 @@ +spanner(); + * + * $date = $spanner->date(new \DateTime('1995-02-04')); + * ``` + * + * ``` + * // Date objects can be cast to strings for easy display. + * echo (string) $date; + * ``` + */ +class Date implements ValueInterface +{ + const FORMAT = 'Y-m-d'; + + /** + * @var \DateTimeInterface + */ + protected $value; + + /** + * @param \DateTimeInterface $value The date value. + */ + public function __construct(\DateTimeInterface $value) + { + $this->value = $value; + } + + /** + * Get the underlying `\DateTimeInterface` implementation. + * + * Example: + * ``` + * $dateTime = $date->get(); + * ``` + * + * @return \DateTimeInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * Example: + * ``` + * echo $date->type(); + * ``` + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_DATE; + } + + /** + * Format the value as a string. + * + * Example: + * ``` + * echo $date->formatAsString(); + * ``` + * + * @return string + */ + public function formatAsString() + { + return $this->value->format(self::FORMAT); + } + + /** + * Format the value as a string. + * + * @return string + * @access private + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Duration.php b/src/Spanner/Duration.php new file mode 100644 index 000000000000..d8c0b45c510d --- /dev/null +++ b/src/Spanner/Duration.php @@ -0,0 +1,122 @@ +spanner(); + * + * $seconds = 100; + * $nanoSeconds = 000001; + * $duration = $spanner->duration($seconds, $nanoSeconds); + * ``` + * + * ``` + * // Duration objects can be cast to strings for easy display. + * 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 $date->formatAsString(); + * ``` + * + * @return string + */ + public function formatAsString() + { + return json_encode($this->get()); + } + + /** + * Format the value as a string. + * + * @return string + * @access private + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Instance.php b/src/Spanner/Instance.php index 5f9f22dfddc8..622d982c4ea0 100644 --- a/src/Spanner/Instance.php +++ b/src/Spanner/Instance.php @@ -31,6 +31,11 @@ * * Example: * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * * $instance = $spanner->instance('my-instance'); * ``` */ @@ -59,6 +64,11 @@ class Instance */ private $name; + /** + * @var bool + */ + private $returnInt64AsObject; + /** * @var array */ @@ -77,6 +87,9 @@ class Instance * @param SessionPoolInterface $sessionPool The session pool implementation. * @param string $projectId The project ID. * @param string $name The instance name. + * @param bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit platform + * compatibility. **Defaults to** false. * @param array $info [optional] A representation of the instance object. */ public function __construct( @@ -84,12 +97,14 @@ public function __construct( SessionPoolInterface $sessionPool, $projectId, $name, + $returnInt64AsObject = false, array $info = [] ) { $this->connection = $connection; $this->sessionPool = $sessionPool; $this->projectId = $projectId; $this->name = $name; + $this->returnInt64AsObject = $returnInt64AsObject; $this->info = $info; $this->iam = new Iam( new IamInstance($this->connection), @@ -143,7 +158,7 @@ public function info(array $options = []) * Example: * ``` * if ($instance->exists()) { - * echo 'The instance exists!'; + * echo 'Instance exists!'; * } * ``` * @@ -169,6 +184,10 @@ public function exists(array $options = []) * $info = $instance->reload(); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.GetInstanceRequest GetInstanceRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration options. * @return array */ @@ -190,9 +209,8 @@ public function reload(array $options = []) * * Example: * ``` - * $instance = $spanner->createInstance($config, 'my-new-instance'); * if ($instance->state() === Instance::STATE_READY) { - * // do stuff + * echo 'Instance is ready!'; * } * ``` * @@ -213,15 +231,26 @@ public function state(array $options = []) * * Example: * ``` - * todo + * $instance->update([ + * 'displayName' => 'My Instance', + * 'nodeCount' => 4 + * ]); * ``` * - * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1 Update Instance + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#updateinstancerequest UpdateInstanceRequest + * @codingStandardsIgnoreEnd * - * @param array $options { + * @param array $options [optional] { * Configuration options * - * @type Configuration $config The configuration to move the instante to. + * @type string $displayName The descriptive name for this instance as + * it appears in UIs. **Defaults to** the value of $name. + * @type int $nodeCount The number of nodes allocated to this instance. + * **Defaults to** `1`. + * @type array $labels For more information, see + * [Using labels to organize Google Cloud Platform resources](https://goo.gl/xmQnxf). + * } * @return void * @throws \InvalidArgumentException */ @@ -231,30 +260,14 @@ public function update(array $options = []) $options += [ 'displayName' => $info['displayName'], - 'nodeCount' => $info['nodeCount'], - 'config' => null, + 'nodeCount' => (isset($info['nodeCount'])) ? $info['nodeCount'] : null, 'labels' => (isset($info['labels'])) ? $info['labels'] : [] ]; - $config = $info['config']; - if ($options['config']) { - if (!($options['config'] instanceof Configuration)) { - throw new \InvalidArgumentException( - 'Given configuration is not an instance of Configuration.' - ); - } - - $config = InstanceAdminClient::formatInstanceConfigName( - $this->projectId, - $options['config']->name() - ); - } - $this->connection->updateInstance([ 'name' => $this->fullyQualifiedInstanceName(), - 'config' => $config, ] + $options); } @@ -266,6 +279,10 @@ public function update(array $options = []) * $instance->delete(); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.instance.v1#deleteinstancerequest DeleteInstanceRequest + * @codingStandardsIgnoreEnd + * * @param array $options [optional] Configuration options. * @return void */ @@ -284,7 +301,9 @@ public function delete(array $options = []) * $database = $instance->createDatabase('my-database'); * ``` * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/create Create Database + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.admin.database.v1#createdatabaserequest CreateDatabaseRequest + * @codingStandardsIgnoreEnd * * @param string $name The database name. * @param array $options [optional] { @@ -302,7 +321,7 @@ public function createDatabase($name, array $options = []) $statement = sprintf('CREATE DATABASE `%s`', $name); - $res = $this->connection->createDatabase([ + $this->connection->createDatabase([ 'instance' => $this->fullyQualifiedInstanceName(), 'createStatement' => $statement, 'extraStatements' => $options['statements'] @@ -329,7 +348,8 @@ public function database($name) $this, $this->sessionPool, $this->projectId, - $name + $name, + $this->returnInt64AsObject ); } @@ -341,27 +361,35 @@ public function database($name) * $databases = $instance->databases(); * ``` * - * @todo implement pagination! - * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases/list List Databases + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.database.v1#listdatabasesrequest ListDatabasesRequest + * @codingStandardsIgnoreEnd * * @param array $options Configuration options. * @return \Generator */ public function databases(array $options = []) { - $res = $this->connection->listDatabases($options + [ - 'instance' => $this->fullyQualifiedInstanceName(), - ]); - - $databases = []; - if (isset($res['databases'])) { - foreach ($res['databases'] as $database) { - yield $this->database( - DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']) - ); + $pageToken = null; + do { + $res = $this->connection->listDatabases($options + [ + 'instance' => $this->fullyQualifiedInstanceName(), + 'pageToken' => $pageToken + ]); + + $databases = []; + if (isset($res['databases'])) { + foreach ($res['databases'] as $database) { + yield $this->database( + DatabaseAdminClient::parseDatabaseFromDatabaseName($database['name']) + ); + } } - } + + $pageToken = (isset($res['nextPageToken'])) + ? $res['nextPageToken'] + : null; + } while($pageToken); } /** diff --git a/src/Spanner/KeyRange.php b/src/Spanner/KeyRange.php index 729e8c43408f..4407745f00ac 100644 --- a/src/Spanner/KeyRange.php +++ b/src/Spanner/KeyRange.php @@ -17,75 +17,210 @@ namespace Google\Cloud\Spanner; +/** + * Represents a Google Cloud Spanner KeyRange. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.KeyRange KeyRange + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * // Create a KeyRange for all people named Bob, born in 1969. + * $start = $spanner->date(new \DateTime('1969-01-01')); + * $end = $spanner->date(new \DateTime('1969-12-31')); + * + * $range = $spanner->keyRange([ + * 'startType' => KeyRange::TYPE_CLOSED, + * 'start' => ['Bob', $start], + * 'endType' => KeyRange::TYPE_CLOSED, + * 'end' => ['Bob', $end] + * ]); + * ``` + */ class KeyRange { + const TYPE_OPEN = 'open'; + const TYPE_CLOSED = 'closed'; + /** - * @var mixed + * @var array */ - private $startOpen; + private $types = []; /** - * @var mixed + * @var array */ - private $startClosed; + private $range = []; /** - * @var mixed + * @var array */ - private $endOpen; + private $definition = [ + self::TYPE_OPEN => [ + 'start' => 'startOpen', + 'end' => 'endOpen' + ], + self::TYPE_CLOSED => [ + 'start' => 'startClosed', + 'end' => 'endClosed' + ] + ]; /** - * @var mixed + * Create a KeyRange. + * + * @param array $options [optional] { + * Configuration Options. + * + * @type string $startType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $start The key with which to start the range. + * @type string $endType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $end The key with which to end the range. + * } */ - private $endClosed; - - public function __construct(array $range) + public function __construct(array $options = []) { - $this->startOpen = (isset($range['startOpen'])) - ? $range['startOpen'] - : null; - - $this->startClosed = (isset($range['startClosed'])) - ? $range['startClosed'] - : null; + $options = array_filter($options + [ + 'startType' => null, + 'start' => [], + 'endType' => null, + 'end' => [] + ]); - $this->endOpen = (isset($range['endOpen'])) - ? $range['endOpen'] - : null; + if (isset($options['startType']) && isset($options['start'])) { + $this->setStart($options['startType'], $options['start']); + } - $this->endClosed = (isset($range['endClosed'])) - ? $range['endClosed'] - : null; + if (isset($options['endType']) && isset($options['end'])) { + $this->setEnd($options['endType'], $options['end']); + } + } + /** + * Get the range start. + * + * Example: + * ``` + * $start = $range->start(); + * ``` + * + * @return array + */ + public function start() + { + $type = $this->types['start']; + return $this->range[$this->definition[$type]['start']]; } - public function setStartOpen($startOpen) + /** + * Set the range start. + * + * Example: + * ``` + * $range->setStart(KeyRange::TYPE_OPEN, ['Bob']); + * ``` + * + * @param string $type Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed + * correctness. + * @param array $start The start of the key range. + * @return void + */ + public function setStart($type, array $start) { - $this->startOpen = $startOpen; + if (!in_array($type, array_keys($this->definition))) { + throw new \InvalidArgumentException(sprintf( + 'Invalid KeyRange type. Allowed values are %s', + implode(', ', array_keys($this->definition)) + )); + } + + $rangeKey = $this->definition[$type]['start']; + + $this->types['start'] = $type; + $this->range[$rangeKey] = $start; } - public function setStartClosed($startClosed) + /** + * Get the range end. + * + * Example: + * ``` + * $end = $range->end(); + * ``` + * + * @return array + */ + public function end() { - $this->startClosed = $startClosed; + $type = $this->types['end']; + return $this->range[$this->definition[$type]['end']]; } - public function setEndOpen($endOpen) + /** + * Set the range end. + * + * Example: + * ``` + * $range->setEnd(KeyRange::TYPE_CLOSED, ['Jill']); + * ``` + * + * @param string $type Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for guaranteed + * correctness. + * @param array $end The end of the key range. + * @return void + */ + public function setEnd($type, array $end) { - $this->endOpen = $endOpen; + if (!in_array($type, array_keys($this->definition))) { + throw new \InvalidArgumentException(sprintf( + 'Invalid KeyRange type. Allowed values are %s', + implode(', ', array_keys($this->definition)) + )); + } + + $rangeKey = $this->definition[$type]['end']; + + $this->types['end'] = $type; + $this->range[$rangeKey] = $end; } - public function setEndClosed($endClosed) + /** + * Get the start and end types + * + * Example: + * ``` + * $types = $range->types(); + * ``` + * + * @return array + */ + public function types() { - $this->endClosed = $endClosed; + return $this->types; } + /** + * Returns an API-compliant representation of a KeyRange. + * + * @return array + * @access private + */ public function keyRangeObject() { - return [ - 'startOpen' => $this->startOpen, - 'startClosed' => $this->startClosed, - 'endOpen' => $this->endOpen, - 'endClosed' => $this->endClosed, - ]; + if (count($this->range) !== 2) { + throw new \BadMethodCallException('Key Range must supply a start and an end'); + } + + return $this->range; } } diff --git a/src/Spanner/KeySet.php b/src/Spanner/KeySet.php index 1de1af5c4f1e..7aa199291624 100644 --- a/src/Spanner/KeySet.php +++ b/src/Spanner/KeySet.php @@ -22,6 +22,16 @@ /** * Represents a Google Cloud Spanner KeySet. * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $keySet = $spanner->keySet(); + * ``` + * * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#keyset KeySet */ class KeySet @@ -43,6 +53,18 @@ class KeySet */ private $all; + /** + * Create a KeySet. + * + * @param array $options [optional] { + * @type array $keys A list of specific keys. Entries in keys should + * have exactly as many elements as there are columns in the + * primary or index key with which this KeySet is used. + * @type KeyRange[] $ranges A list of Key Ranges. + * @type bool $all If true, KeySet will match all keys in a table. + * **Defaults to** `false`. + * } + */ public function __construct(array $options = []) { $options += [ @@ -58,11 +80,53 @@ public function __construct(array $options = []) $this->all = (bool) $options['all']; } + /** + * Fetch the KeyRanges + * + * Example: + * ``` + * $ranges = $keySet->ranges(); + * ``` + * + * @return KeyRange[] + */ + public function ranges() + { + return $this->ranges; + } + + + /** + * Add a single KeyRange. + * + * Example: + * ``` + * $range = $spanner->keyRange(); + * $keySet->addRange($range); + * ``` + * + * @param KeyRange $range A KeyRange instance. + * @return void + */ public function addRange(KeyRange $range) { $this->ranges[] = $range; } + /** + * Set the KeySet's KeyRanges. + * + * Any existing KeyRanges will be overridden. + * + * Example: + * ``` + * $range = $spanner->keyRange(); + * $keySet->setRanges([$range]); + * ``` + * + * @param KeyRange[] $ranges An array of KeyRange objects. + * @return void + */ public function setRanges(array $ranges) { $this->validateBatch($ranges, KeyRange::class); @@ -70,21 +134,96 @@ public function setRanges(array $ranges) $this->ranges = $ranges; } + /** + * Fetch the keys. + * + * Example: + * ``` + * $keys = $keySet->keys(); + * ``` + * + * @return mixed[] + */ + public function keys() + { + return $this->keys; + } + + /** + * Add a single key. + * + * A Key should have exactly as many elements as there are columns in the + * primary or index key with which this KeySet is used. + * + * Example: + * ``` + * $keySet->addKey('Bob'); + * ``` + * + * @param mixed $key The Key to add. + * @return void + */ public function addKey($key) { $this->keys[] = $key; } + /** + * Set the KeySet keys. + * + * Any existing keys will be overridden. + * + * Example: + * ``` + * $keySet->setKeys(['Bob', 'Jill']); + * ``` + * + * @param mixed[] $keys + * @return void + */ public function setKeys(array $keys) { $this->keys = $keys; } - public function setAll($all) + /** + * Get the value of Match All. + * + * Example: + * ``` + * if ($keySet->matchAll()) { + * echo "All keys will match"; + * } + * ``` + * + * @return bool + */ + public function matchAll() + { + return $this->all; + } + + /** + * Choose whether the KeySet should match all keys in a table. + * + * Example: + * ``` + * $keySet->matchAll(true); + * ``` + * + * @param bool $all If true, all keys in a table will be matched. + * @return void + */ + public function setMatchAll($all) { $this->all = (bool) $all; } + /** + * Format a KeySet object for use in the Spanner API. + * + * @access private + */ public function keySetObject() { $ranges = []; diff --git a/src/Spanner/Operation.php b/src/Spanner/Operation.php index 0e05dd90f20a..b49348ccfb2b 100644 --- a/src/Spanner/Operation.php +++ b/src/Spanner/Operation.php @@ -17,25 +17,32 @@ namespace Google\Cloud\Spanner; +use Google\Cloud\ArrayTrait; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Session\Session; +use Google\Cloud\Spanner\Session\SessionPoolInterface; use Google\Cloud\ValidateTrait; -use RuntimeException; /** * Common interface for running operations against Google Cloud Spanner. This * class is intended for internal use by the client library only. Implementors * should access these operations via {@see Google\Cloud\Spanner\Database} or * {@see Google\Cloud\Spanner\Transaction}. + * + * Usage examples may be found in classes making use of this class: + * * {@see Google\Cloud\Spanner\Database} + * * {@see Google\Cloud\Spanner\Transaction} */ class Operation { + use ArrayTrait; use ValidateTrait; const OP_INSERT = 'insert'; const OP_UPDATE = 'update'; const OP_INSERT_OR_UPDATE = 'insertOrUpdate'; const OP_REPLACE = 'replace'; + const OP_DELETE = 'delete'; /** * @var ConnectionInterface @@ -43,22 +50,21 @@ class Operation private $connection; /** - * @var Instance + * @var ValueMapper */ - private $instance; + private $mapper; /** * @param ConnectionInterface $connection A connection to Google Cloud * Spanner. - * @param Instance $instance The current Cloud Spanner instance. + * @param bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit platform + * compatibility. */ - public function __construct( - ConnectionInterface $connection, - Instance $instance, - Database $database - ) { + public function __construct(ConnectionInterface $connection, $returnInt64AsObject) + { $this->connection = $connection; - $this->instance = $instance; + $this->mapper = new ValueMapper($returnInt64AsObject); } /** @@ -72,11 +78,13 @@ public function __construct( */ public function mutation($operation, $table, $mutation) { + $mutation = $this->arrayFilterRemoveNull($mutation); + return [ $operation => [ 'table' => $table, 'columns' => array_keys($mutation), - 'values' => array_values($mutation) + 'values' => $this->mapper->encodeValuesAsSimpleType(array_values($mutation)) ] ]; } @@ -85,15 +93,15 @@ public function mutation($operation, $table, $mutation) * Create a formatted delete mutation. * * @param string $table The table name. - * @param array $keySet [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). + * @param KeySet $keySet The keys to delete. * @return array */ - public function deleteMutation($table, $keySet) + public function deleteMutation($table, KeySet $keySet) { return [ - 'delete' => [ + self::OP_DELETE => [ 'table' => $table, - 'keySet' => $keySet + 'keySet' => $this->flattenKeySet($keySet), ] ]; } @@ -103,30 +111,26 @@ public function deleteMutation($table, $keySet) * * @codingStandardsIgnoreStart * @param Session $session The session ID to use for the commit. - * @param array $mutations The mutations to commit. - * @param array $options [optional] Configuration options. - * @return array [CommitResponse](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.CommitResponse) - * @codingStandardsIgnoreEnd + * @param Transaction $transaction The transaction to commit. + * @param array $options [optional] { + * Configuration options. + * + * @type string $transactionId + * } + * @return Timestamp The commit Timestamp. */ public function commit(Session $session, array $mutations, array $options = []) { - if (!isset($options['transactionId'])) { - $options['singleUseTransaction'] = ['readWrite' => []]; - } - - try { - $res = $this->connection->commit([ - 'mutations' => $mutations, - 'session' => $session->name() - ] + $options); - - return $res; - } catch (\Exception $e) { + $options += [ + 'transactionId' => null + ]; - // maybe do something here? + $res = $this->connection->commit($this->arrayFilterRemoveNull([ + 'mutations' => $mutations, + 'session' => $session->name() + ]) + $options); - throw $e; - } + return $this->mapper->createTimestampWithNanos($res['commitTimestamp']); } /** @@ -158,60 +162,209 @@ public function rollback(Session $session, $transactionId, array $options = []) public function execute(Session $session, $sql, array $options = []) { $options += [ - 'params' => [], - 'paramTypes' => [] + 'parameters' => [], + 'transactionContext' => null ]; + $parameters = $this->pluck('parameters', $options); + $options += $this->mapper->formatParamsForExecuteSql($parameters); + + $context = $this->pluck('transactionContext', $options); + $res = $this->connection->executeSql([ 'sql' => $sql, 'session' => $session->name() ] + $options); - return new Result($res); + return $this->createResult($session, $res, $context); } /** * Lookup rows in a database. * * @param Session $session The session in which to read data. - * @param string $table The table to read from. + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. * @param array $options [optional] { - * Configuration Options + * Configuration Options. * - * @type string $index - * @type array $columns - * @type KeySet $keySet - * @type string $offset - * @type int $limit + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. * } + * @return Result */ - public function read(Session $session, $table, array $options = []) + public function read(Session $session, $table, KeySet $keySet, array $columns, array $options = []) { $options += [ 'index' => null, - 'columns' => [], - 'keySet' => [], - 'offset' => null, 'limit' => null, + 'offset' => null, + 'transactionContext' => null ]; - if (!empty($options['keySet']) && !($options['keySet']) instanceof KeySet) { - throw new RuntimeException('$options.keySet must be an instance of KeySet'); + $context = $this->pluck('transactionContext', $options); + $res = $this->connection->read([ + 'table' => $table, + 'session' => $session->name(), + 'columns' => $columns, + 'keySet' => $this->flattenKeySet($keySet) + ] + $options); + + return $this->createResult($session, $res, $context); + } + + /** + * Create a read/write transaction. + * + * @todo if a transaction is already available on the session, get it instead + * of starting a new one? + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the transaction in. + * @param array $options [optional] Configuration options. + * @return Transaction + */ + public function transaction(Session $session, array $options = []) + { + $res = $this->beginTransaction($session, $options); + return $this->createTransaction($session, $res); + } + + /** + * Create a read-only snapshot transaction. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the snapshot in. + * @param array $options [optional] Configuration options. + * @return Snapshot + */ + public function snapshot(Session $session, array $options = []) + { + $res = $this->beginTransaction($session, $options); + + return $this->createSnapshot($session, $res); + } + + /** + * Execute a service call to begin a transaction or snapshot. + * + * @see https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.BeginTransactionRequest BeginTransactionRequest + * + * @param Session $session The session to start the snapshot in. + * @param array $options [optional] Configuration options. + * @return array + */ + private function beginTransaction(Session $session, array $options = []) + { + $options += [ + 'transactionOptions' => [] + ]; + + return $this->connection->beginTransaction($options + [ + 'session' => $session->name(), + ]); + } + + /** + * Create a Transaction instance from a response object. + * + * @param Session $session The session the transaction belongs to. + * @param array $res The transaction response. + * @return Transaction + */ + private function createTransaction(Session $session, array $res) + { + return new Transaction($this, $session, $res['id']); + } + + /** + * Create a Snapshot instance from a response object. + * + * @param Session $session The session the snapshot belongs to. + * @param array $res The snapshot response. + * @return Snapshot + */ + private function createSnapshot(Session $session, array $res) + { + $timestamp = null; + if (isset($res['readTimestamp'])) { + $timestamp = $this->mapper->createTimestampWithNanos($res['readTimestamp']); + } + + return new Snapshot($this, $session, $res['id'], $timestamp); + } + + /** + * Transform a service read or executeSql response to a friendly result. + * + * @codingStandardsIgnoreStart + * @param Session $session The current session. + * @param array $res [ResultSet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet) + * @param string $transactionContext + * @return Result + * @codingStandardsIgnoreEnd + */ + private function createResult(Session $session, array $res, $transactionContext) + { + $columns = isset($res['metadata']['rowType']['fields']) + ? $res['metadata']['rowType']['fields'] + : []; + + $rows = []; + if (isset($res['rows'])) { + foreach ($res['rows'] as $row) { + $rows[] = $this->mapper->decodeValues($columns, $row); + } } - if (empty($options['keySet'])) { - $options['keySet'] = new KeySet(); - $options['keySet']->setAll(true); + $options = []; + if (isset($res['metadata']['transaction']['id'])) { + if ($transactionContext === SessionPoolInterface::CONTEXT_READ) { + $options['snapshot'] = $this->createSnapshot($session, $res['metadata']['transaction']); + } else { + $options['transaction'] = $this->createTransaction($session, $res['metadata']['transaction']); + } } - $options['keySet'] = $options['keySet']->keySetObject(); + return new Result($res, $rows, $options); + } - $res = $this->connection->read([ - 'table' => $table, - 'session' => $session->name() - ] + $options); + /** + * Convert a KeySet object to an API-ready array. + * + * @param KeySet $keySet The keySet object. + * @return array [KeySet](https://cloud.google.com/spanner/reference/rpc/google.spanner.v1#keyset) + */ + private function flattenKeySet(KeySet $keySet) + { + $keyRanges = $keySet->ranges(); + if ($keyRanges) { + $ranges = []; + foreach ($keyRanges as $range) { + $types = $range->types(); + + $start = $range->start(); + $range->setStart($types['start'], $this->mapper->encodeValuesAsSimpleType($start)); + + $end = $range->end(); + $range->setEnd($types['end'], $this->mapper->encodeValuesAsSimpleType($end)); + + $ranges[] = $range; + } + + $keySet->setRanges($ranges); + } + + $keys = $keySet->keySetObject(); + if (!empty($keys['keys'])) { + $keys['keys'] = $this->mapper->encodeValuesAsSimpleType($keys['keys']); + } - return new Result($res); + return $this->arrayFilterRemoveNull($keys); } /** @@ -224,8 +377,6 @@ public function __debugInfo() { return [ 'connection' => get_class($this->connection), - 'instance' => $this->instance, - 'sessionPool' => $this->sessionPool ]; } } diff --git a/src/Spanner/Result.php b/src/Spanner/Result.php index 35cedd220956..c168438ce963 100644 --- a/src/Spanner/Result.php +++ b/src/Spanner/Result.php @@ -18,9 +18,22 @@ namespace Google\Cloud\Spanner; /** - * @todo should this be more like BigQuery\QueryResults? + * Represent a Google Cloud Spanner lookup result (either read or executeSql). + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * $database = $spanner->connect('my-instance', 'my-database'); + * + * $result = $database->execute('SELECT * FROM Posts'); + * ``` + * + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet ResultSet */ -class Result implements \Iterator +class Result implements \IteratorAggregate { /** * @var array @@ -33,23 +46,33 @@ class Result implements \Iterator private $rows; /** - * @var int + * @var array */ - private $index = 0; + private $options; /** - * @var array $result The query or read result. + * @param array $result The query or read result. + * @param array $rows The rows, formatted and decoded. + * @param array $options Additional result options and info. */ - public function __construct(array $result) + public function __construct(array $result, array $rows, array $options = []) { $this->result = $result; - $this->rows = $this->transformQueryResult($result); + $this->rows = $rows; + $this->options = $options; } /** * Return result metadata * - * @return array [ResultSetMetadata](https://cloud.google.com/spanner/reference/rest/v1/ResultSetMetadata). + * Example: + * ``` + * $metadata = $result->metadata(); + * ``` + * + * @codingStandardsIgnoreStart + * @return array [ResultSetMetadata](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetMetadata). + * @codingStandardsIgnoreEnd */ public function metadata() { @@ -57,17 +80,36 @@ public function metadata() } /** - * Return the rows as represented by the API. + * Return the formatted and decoded rows. * - * For a more easily consumed result in which each row is represented as a - * set of key/value pairs, see {@see Google\Cloud\Spanner\Result::result()}. + * Example: + * ``` + * $rows = $result->rows(); + * ``` * * @return array|null */ public function rows() { - return (isset($this->result['rows'])) - ? $result['rows'] + return $this->rows; + } + + /** + * Return the first row, or null. + * + * Useful when selecting a single row. + * + * Example: + * ``` + * $row = $result->firstRow(); + * ``` + * + * @return array|null + */ + public function firstRow() + { + return (isset($this->rows[0])) + ? $this->rows[0] : null; } @@ -77,90 +119,85 @@ public function rows() * * Stats are not returned by default. * - * @todo explain how to get dem stats. + * Example: + * ``` + * $stats = $result->stats(); + * ``` + * + * ``` + * // Executing a query with stats returned. + * $res = $database->execute('SELECT * FROM Posts', [ + * 'queryMode' => 'PROFILE' + * ]); + * ``` * - * @return array|null [ResultSetStats](https://cloud.google.com/spanner/reference/rest/v1/ResultSetStats). + * @codingStandardsIgnoreStart + * @return array|null [ResultSetStats](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSetStats). + * @codingStandardsIgnoreEnd */ public function stats() { return (isset($this->result['stats'])) - ? $result['stats'] + ? $this->result['stats'] : null; } /** - * Get the entire query or read response as given by the API. + * Returns a transaction which was begun in the read or execute, if one exists. * - * @return array [ResultSet](https://cloud.google.com/spanner/reference/rest/v1/ResultSet). - */ - public function info() - { - return $this->result; - } - - /** - * Transform the response from executeSql or read into a list of rows - * represented as a collection of key/value arrays. + * Example: + * ``` + * $transaction = $result->transaction(); + * ``` * - * @param array $result - * @return array - */ - private function transformQueryResult(array $result) - { - if (!isset($result['rows']) || count($result['rows']) === 0) { - return null; - } - - $cols = []; - foreach (array_keys($result['rows'][0]) as $colIndex) { - $cols[] = $result['metadata']['rowType']['fields'][$colIndex]['name']; - } - - $rows = []; - foreach ($result['rows'] as $row) { - $rows[] = array_combine($cols, $row); - } - - return $rows; - } - - /** - * @access private + * @return Transaction|null */ - public function rewind() + public function transaction() { - $this->index = 0; - } - - /** - * @access private - */ - public function current() - { - return $this->rows[$this->index]; + return (isset($this->options['transaction'])) + ? $this->options['transaction'] + : null; } /** - * @access private + * Returns a snapshot which was begun in the read or execute, if one exists. + * + * Example: + * ``` + * $snapshot = $result->snapshot(); + * ``` + * + * @return Snapshot|null */ - public function key() + public function snapshot() { - return $this->index; + return (isset($this->options['snapshot'])) + ? $this->options['snapshot'] + : null; } /** - * @access private + * Get the entire query or read response as given by the API. + * + * Example: + * ``` + * $info = $result->info(); + * ``` + * + * @codingStandardsIgnoreStart + * @return array [ResultSet](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#google.spanner.v1.ResultSet). + * @codingStandardsIgnoreEnd */ - public function next() + public function info() { - ++$this->index; + return $this->result; } /** * @access private */ - public function valid() + public function getIterator() { - return isset($this->rows[$this->index]); + return new \ArrayIterator($this->rows); } } diff --git a/src/Spanner/Session/Session.php b/src/Spanner/Session/Session.php index b0b811b3765a..f2f86584a7cf 100644 --- a/src/Spanner/Session/Session.php +++ b/src/Spanner/Session/Session.php @@ -156,4 +156,15 @@ public function name() $this->name ); } + + public function __debugInfo() + { + return [ + 'connection' => get_class($this->connection), + 'projectId' => $this->projectId, + 'instance' => $this->instance, + 'database' => $this->database, + 'name' => $this->name, + ]; + } } diff --git a/src/Spanner/Session/SessionClient.php b/src/Spanner/Session/SessionClient.php index 3cb21ff3b22d..7024f198a5f8 100644 --- a/src/Spanner/Session/SessionClient.php +++ b/src/Spanner/Session/SessionClient.php @@ -82,18 +82,23 @@ public function create($instance, $database, array $options = []) $session = null; if (isset($res['name'])) { - $session = new Session( - $this->connection, - $this->projectId, - SpannerClient::parseInstanceFromSessionName($res['name']), - SpannerClient::parseDatabaseFromSessionName($res['name']), - SpannerClient::parseSessionFromSessionName($res['name']) - ); + $session = $this->session($res['name']); } return $session; } + public function session($sessionName) + { + return new Session( + $this->connection, + $this->projectId, + SpannerClient::parseInstanceFromSessionName($sessionName), + SpannerClient::parseDatabaseFromSessionName($sessionName), + SpannerClient::parseSessionFromSessionName($sessionName) + ); + } + public function __debugInfo() { return [ diff --git a/src/Spanner/Session/SessionPoolInterface.php b/src/Spanner/Session/SessionPoolInterface.php index 28662a090ae9..1294f2571d6a 100644 --- a/src/Spanner/Session/SessionPoolInterface.php +++ b/src/Spanner/Session/SessionPoolInterface.php @@ -19,8 +19,8 @@ interface SessionPoolInterface { - const CONTEXT_READ = 'read'; - const CONTEXT_READWRITE = 'readWrite'; + const CONTEXT_READ = 'r'; + const CONTEXT_READWRITE = 'rw'; public function session($instance, $database, $context, array $options = []); } diff --git a/src/Spanner/Snapshot.php b/src/Spanner/Snapshot.php new file mode 100644 index 000000000000..3a7d241e41e1 --- /dev/null +++ b/src/Spanner/Snapshot.php @@ -0,0 +1,85 @@ +spanner(); + * + * $database = $spanner->connect('my-instance', 'my-database'); + * $snapshot = $database->snapshot(); + * ``` + */ +class Snapshot +{ + use TransactionReadTrait; + + /** + * @var Timestamp + */ + private $readTimestamp; + + /** + * @param Operation $operation The Operation instance. + * @param Session $session The session to use for spanner interactions. + * @param string $transactionId The Transaction ID. + * @param Timestamp $readTimestamp [optional] The read timestamp. + */ + public function __construct( + Operation $operation, + Session $session, + $transactionId, + Timestamp $readTimestamp = null + ) { + $this->operation = $operation; + $this->session = $session; + $this->transactionId = $transactionId; + $this->readTimestamp = $readTimestamp; + $this->context = SessionPoolInterface::CONTEXT_READWRITE; + } + + /** + * Retrieve the Read Timestamp. + * + * For snapshot read-only transactions, the read timestamp chosen for the + * transaction. + * + * Example: + * ``` + * $timestamp = $transaction->readTimestamp(); + * ``` + * + * @return Timestamp + */ + public function readTimestamp() + { + return $this->readTimestamp; + } +} diff --git a/src/Spanner/SpannerClient.php b/src/Spanner/SpannerClient.php index 582cd0eb7359..0c2e057ffb38 100644 --- a/src/Spanner/SpannerClient.php +++ b/src/Spanner/SpannerClient.php @@ -19,6 +19,7 @@ use Google\Cloud\ClientTrait; use Google\Cloud\Exception\NotFoundException; +use Google\Cloud\Int64; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Connection\Grpc; use Google\Cloud\Spanner\Session\SessionClient; @@ -26,6 +27,26 @@ use Google\Cloud\ValidateTrait; use google\spanner\admin\instance\v1\Instance\State; +/** + * Google Cloud Spanner is a highly scalable, transactional, managed, NewSQL + * database service. Find more information at + * [Google Cloud Spanner docs](https://cloud.google.com/spanner/). + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * ``` + * + * ``` + * // SpannerClient can be instantiated directly. + * use Google\Cloud\Spanner\SpannerClient; + * + * $spanner = new SpannerClient(); + * ``` + */ class SpannerClient { use ClientTrait; @@ -52,6 +73,11 @@ class SpannerClient */ protected $sessionPool; + /** + * @var bool + */ + private $returnInt64AsObject; + /** * Create a Spanner client. * @@ -72,60 +98,85 @@ class SpannerClient * @type int $retries Number of retries for a failed request. * **Defaults to** `3`. * @type array $scopes Scopes to be used for the request. + * @type bool $returnInt64AsObject If true, 64 bit integers will be + * returned as a {@see Google\Cloud\Int64} object for 32 bit + * platform compatibility. **Defaults to** false. * } * @throws Google\Cloud\Exception\GoogleException */ public function __construct(array $config = []) { - if (!isset($config['scopes'])) { - $config['scopes'] = [ + $config += [ + 'scopes' => [ self::FULL_CONTROL_SCOPE, self::ADMIN_SCOPE - ]; - } + ], + 'returnInt64AsObject' => false + ]; $this->connection = new Grpc($this->configureAuthentication($config)); $this->sessionClient = new SessionClient($this->connection, $this->projectId); $this->sessionPool = new SimpleSessionPool($this->sessionClient); + + $this->returnInt64AsObject = $config['returnInt64AsObject']; } /** - * List all available configurations + * List all available configurations. * * Example: * ``` * $configurations = $spanner->configurations(); * ``` * - * @todo implement pagination! - * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instanceConfigs/list List Configs + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#google.spanner.admin.instance.v1.ListInstanceConfigsRequest ListInstanceConfigsRequest + * @codingStandardsIgnoreEnd * + * @param array $options [optional] Configuration Options. * @return Generator */ - public function configurations() + public function configurations(array $options = []) { - $res = $this->connection->listConfigs([ - 'projectId' => InstanceAdminClient::formatProjectName($this->projectId) - ]); + $pageToken = null; + do { + $res = $this->connection->listConfigs([ + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), + 'pageToken' => $pageToken + ] + $options); - if (isset($res['instanceConfigs'])) { - foreach ($res['instanceConfigs'] as $config) { - $name = InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config['name']); - yield $this->configuration($name, $config); + if (isset($res['instanceConfigs'])) { + foreach ($res['instanceConfigs'] as $config) { + $name = InstanceAdminClient::parseInstanceConfigFromInstanceConfigName($config['name']); + yield $this->configuration($name, $config); + } } - } + + $pageToken = (isset($res['nextPageToken'])) + ? $res['nextPageToken'] + : null; + } while($pageToken); } /** - * Get a configuration by its name + * Get a configuration by its name. + * + * NOTE: This method does not execute a service request and does not verify + * the existence of the given configuration. Unless you know with certainty + * that the configuration exists, it is advised that you use + * {@see Google\Cloud\Spanner\Configuration::exists()} to verify existence + * before attempting to use the configuration. * * Example: * ``` * $configuration = $spanner->configuration($configurationName); * ``` * + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#getinstanceconfigrequest GetInstanceConfigRequest + * @codingStandardsIgnoreEnd + * * @param string $name The Configuration name. * @param array $config [optional] The configuration details. * @return Configuration @@ -136,16 +187,16 @@ public function configuration($name, array $config = []) } /** - * Create an instance + * Create a new instance. * * Example: * ``` - * $instance = $spanner->createInstance($configuration, 'my-application-instance'); + * $instance = $spanner->createInstance($configuration, 'my-instance'); * ``` * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/create Create Instance - * * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#createinstancerequest CreateInstanceRequest + * * @param Configuration $config The configuration to use * @param string $name The instance name * @param array $options [optional] { @@ -153,8 +204,8 @@ public function configuration($name, array $config = []) * * @type string $displayName **Defaults to** the value of $name. * @type int $nodeCount **Defaults to** `1`. - * @type int $state **Defaults to** - * @type array $labels [Using labels to organize Google Cloud Platform resources](https://cloudplatform.googleblog.com/2015/10/using-labels-to-organize-Google-Cloud-Platform-resources.html). + * @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 Instance * @codingStandardsIgnoreEnd @@ -164,24 +215,28 @@ public function createInstance(Configuration $config, $name, array $options = [] $options += [ 'displayName' => $name, 'nodeCount' => self::DEFAULT_NODE_COUNT, - 'state' => State::CREATING, 'labels' => [] ]; - $res = $this->connection->createInstance($options + [ + // This must always be set to CREATING, so overwrite anything else. + $options['state'] = State::CREATING; + + $res = $this->connection->createInstance([ + 'instanceId' => $name, 'name' => InstanceAdminClient::formatInstanceName($this->projectId, $name), + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), 'config' => InstanceAdminClient::formatInstanceConfigName($this->projectId, $config->name()) - ]); + ] + $options); return $this->instance($name); } /** - * Lazily instantiate an instance + * Lazily instantiate an instance. * * Example: * ``` - * $instance = $spanner->instance('my-application-instance'); + * $instance = $spanner->instance('my-instance'); * ``` * * @param string $name The instance name @@ -194,33 +249,11 @@ public function instance($name, array $instance = []) $this->sessionPool, $this->projectId, $name, + $this->returnInt64AsObject, $instance ); } - /** - * Connect to a database to run queries or mutations. - * - * Example: - * ``` - * $database = $spanner->connect('my-application-instance', 'my-application-database'); - * ``` - * - * @param Instance|string $instance The instance object or instance name. - * @param string $name The database name. - * @return Database - */ - public function connect($instance, $name) - { - if (is_string($instance)) { - $instance = $this->instance($instance); - } - - $database = $instance->database($name); - - return $database; - } - /** * List instances in the project * @@ -231,7 +264,9 @@ public function connect($instance, $name) * * @todo implement pagination! * - * @see https://cloud.google.com/spanner/reference/rest/v1/projects.instances/list List Instances + * @codingStandardsIgnoreStart + * @see https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.admin.instance.v1#listinstancesrequest ListInstancesRequest + * @codingStandardsIgnoreEnd * * @param array $options [optional] Configuration options * @return Generator @@ -243,7 +278,7 @@ public function instances(array $options = []) ]; $res = $this->connection->listInstances($options + [ - 'projectId' => $this->projectId, + 'projectId' => InstanceAdminClient::formatProjectName($this->projectId), ]); if (isset($res['instances'])) { @@ -256,6 +291,29 @@ public function instances(array $options = []) } } + /** + * Connect to a database to run queries or mutations. + * + * Example: + * ``` + * $database = $spanner->connect('my-instance', 'my-application-database'); + * ``` + * + * @param Instance|string $instance The instance object or instance name. + * @param string $name The database name. + * @return Database + */ + public function connect($instance, $name) + { + if (is_string($instance)) { + $instance = $this->instance($instance); + } + + $database = $instance->database($name); + + return $database; + } + /** * Create a new KeySet object * @@ -276,12 +334,107 @@ public function keySet(array $options = []) /** * Create a new KeyRange object * - * @param array $range [optional] The key range data. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $startType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $start The key with which to start the range. + * @type string $endType Either "open" or "closed". Use constants + * `KeyRange::TYPE_OPEN` and `KeyRange::TYPE_CLOSED` for + * guaranteed correctness. + * @type array $end The key with which to end the range. + * } * @return KeyRange */ - public function keyRange(array $range = []) + public function keyRange(array $options = []) + { + return new KeyRange($options); + } + + /** + * Create a Bytes object. + * + * Example: + * ``` + * $bytes = $spanner->bytes('hello world'); + * ``` + * + * @param string|resource|StreamInterface $value The bytes value. + * @return Bytes + */ + public function bytes($bytes) + { + return new Bytes($bytes); + } + + /** + * Create a Date object. + * + * Example: + * ``` + * $date = $spanner->date(new \DateTime('1995-02-04')); + * ``` + * + * @param \DateTimeInterface $value The date value. + * @return Date + */ + public function date(\DateTimeInterface $date) + { + return new Date($date); + } + + /** + * Create a Timestamp object. + * + * Example: + * ``` + * $timestamp = $spanner->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')); + * ``` + * + * @param \DateTimeInterface $value The timestamp value. + * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp. + * @return Timestamp + */ + public function timestamp(\DateTimeInterface $timestamp, $nanoSeconds = null) + { + return new Timestamp($timestamp, $nanoSeconds); + } + + /** + * Create an Int64 object. This can be used to work with 64 bit integers as + * a string value while on a 32 bit platform. + * + * Example: + * ``` + * $int64 = $spanner->int64('9223372036854775807'); + * ``` + * + * @param string $value + * @return Int64 + */ + public function int64($value) + { + return new Int64($value); + } + + /** + * Create a Duration object. + * + * Example: + * ``` + * $duration = $spanner->duration(100, 00001); + * ``` + * + * @param int $seconds The number of seconds in the duration. + * @param int $nanos [optional] The number of nanoseconds in the duration. + * **Defaults to** `0`. + * @return Duration + */ + public function duration($seconds, $nanos = 0) { - return new KeyRange($range); + return new Duration($seconds, $nanos); } /** diff --git a/src/Spanner/Timestamp.php b/src/Spanner/Timestamp.php new file mode 100644 index 000000000000..8968315d9d0e --- /dev/null +++ b/src/Spanner/Timestamp.php @@ -0,0 +1,100 @@ +timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')); + * ``` + */ +class Timestamp implements ValueInterface +{ + const FORMAT = 'Y-m-d\TH:i:s.u\Z'; + const FORMAT_INTERPOLATE = 'Y-m-d\TH:i:s.%\s\Z'; + + /** + * @var \DateTimeInterface + */ + private $value; + + /** + * @var int + */ + private $nanoSeconds; + + /** + * @param \DateTimeInterface $value The timestamp value. + * @param int $nanoSeconds [optional] The number of nanoseconds in the timestamp. + */ + public function __construct(\DateTimeInterface $value, $nanoSeconds = null) + { + $this->value = $value; + $this->nanoSeconds = $nanoSeconds ?: (int) $this->value->format('u'); + } + + /** + * Get the underlying `\DateTimeInterface` implementation. + * + * @return \DateTimeInterface + */ + public function get() + { + return $this->value; + } + + /** + * Get the type. + * + * @return string + */ + public function type() + { + return ValueMapper::TYPE_TIMESTAMP; + } + + /** + * Format the value as a string. + * + * @return string + */ + public function formatAsString() + { + $this->value->setTimezone(new \DateTimeZone('UTC')); + $ns = str_pad((string) $this->nanoSeconds, 6, '0', STR_PAD_LEFT); + return sprintf($this->value->format(self::FORMAT_INTERPOLATE), $ns); + } + + /** + * Format the value as a string. + * + * @return string + */ + public function __toString() + { + return $this->formatAsString(); + } +} diff --git a/src/Spanner/Transaction.php b/src/Spanner/Transaction.php index 638330f7a515..d3bfa0b0e724 100644 --- a/src/Spanner/Transaction.php +++ b/src/Spanner/Transaction.php @@ -22,228 +22,382 @@ use RuntimeException; /** - * Enabled interaction with Google Cloud Spanner inside a Transaction. + * Manages interaction with Google Cloud Spanner inside a Transaction. + * + * Transactions can be started via + * {@see Google\Cloud\Spanner\Database::runTransaction()} (recommended) or via + * {@see Google\Cloud\Spanner\Database::transaction()}. Transactions should + * always call {@see Google\Cloud\Spanner\Transaction::commit()} or + * {@see Google\Cloud\Spanner\Transaction::rollback()} to ensure that locks are + * released in a timely manner. + * + * If you do not plan on performing any writes in your transaction, a + * {@see Google\Cloud\Spanner\Snapshot} is a better solution which does not + * require a commit or rollback and does not lock any data. + * + * Transactions may raise {@see Google\Cloud\Exception\AbortedException} errors + * when the transaction cannot complete for any reason. In this case, the entire + * operation (all reads and writes) should be reapplied atomically. Google Cloud + * PHP handles this transparently when using + * {@see Google\Cloud\Spanner\Database::runTransaction()}. In other cases, it is + * highly recommended that applications implement their own retry logic. + * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $spanner = $cloud->spanner(); + * + * $database = $spanner->connect('my-instance', 'my-database'); + * + * $database->runTransaction(function (Transaction $t) { + * // do stuff. + * + * $t->commit(); + * }); + * ``` + * + * ``` + * // Get a transaction to manage manually. + * $transaction = $database->transaction(); + * ``` */ class Transaction { - /** - * @var Operation - */ - private $operation; - - /** - * @var Session - */ - private $session; + use TransactionReadTrait; - /** - * @var string - */ - private $context; + const STATE_ACTIVE = 0; + const STATE_ROLLED_BACK = 1; + const STATE_COMMITTED = 2; /** - * @var string - */ - private $transactionId; - - /** - * @var string + * @var array */ - private $readTimestamp; + private $mutations = []; /** - * @var array + * @var int */ - private $mutations = []; + private $state = self::STATE_ACTIVE; /** * @param Operation $operation The Operation instance. * @param Session $session The session to use for spanner interactions. - * @param string $context The Transaction context. - * @param array $transaction Transaction details. + * @param string $transactionId The Transaction ID. */ public function __construct( Operation $operation, Session $session, - $context, - array $transaction + $transactionId ) { $this->operation = $operation; $this->session = $session; - $this->context = $context; - $this->transactionId = $transaction['id']; - $this->readTimestamp = (isset($transaction['readTimestamp'])) - ? $transaction['readTimestamp'] - : null; + $this->transactionId = $transactionId; + $this->context = SessionPoolInterface::CONTEXT_READWRITE; } /** * Enqueue an insert mutation. * + * Example: + * ``` + * $transaction->insert('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ]); + * ``` + * * @param string $table The table to insert into. * @param array $data The data to insert. - * @return void + * @return Transaction The transaction, to enable method chaining. */ public function insert($table, array $data) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + return $this->insertBatch($table, [$data]); + } - $this->mutations[] = $this->operation->mutation(Operation::OP_INSERT, $table, $data); + /** + * Enqueue one or more insert mutations. + * + * Example: + * ``` + * $transaction->insertBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ] + * ]); + * ``` + * + * @param string $table The table to insert into. + * @param array $dataSet The data to insert. + * @return Transaction The transaction, to enable method chaining. + */ + public function insertBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_INSERT, $table, $dataSet); + + return $this; } /** * Enqueue an update mutation. * + * Example: + * ``` + * $transaction->update('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post [Updated!]', + * 'content' => 'Modified Content' + * ]); + * ``` + * * @param string $table The table to update. * @param array $data The data to update. - * @return void + * @return Transaction The transaction, to enable method chaining. */ public function update($table, array $data) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + return $this->updateBatch($table, [$data]); + } - $this->mutations[] = $this->operation->mutation(Operation::OP_UPDATE, $table, $data); + /** + * Enqueue one or more update mutations. + * + * Example: + * ``` + * $transaction->updateBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post [Updated!]', + * 'content' => 'Modified Content' + * ] + * ]); + * ``` + * + * @param string $table The table to update. + * @param array $dataSet The data to update. + * @return Transaction The transaction, to enable method chaining. + */ + public function updateBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_UPDATE, $table, $dataSet); + + return $this; } /** * Enqueue an insert or update mutation. * + * Example: + * ``` + * $transaction->insertOrUpdate('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ]); + * ``` + * * @param string $table The table to insert into or update. * @param array $data The data to insert or update. - * @return void + * @return Transaction The transaction, to enable method chaining. */ public function insertOrUpdate($table, array $data) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + return $this->insertOrUpdateBatch($table, [$data]); + } - $this->mutations[] = $this->operation->mutation(Operation::OP_INSERT_OR_UPDATE, $table, $data); + /** + * Enqueue one or more insert or update mutations. + * + * Example: + * ``` + * $transaction->insertOrUpdateBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post', + * 'content' => 'Hello World' + * ] + * ]); + * ``` + * + * @param string $table The table to insert into or update. + * @param array $dataSet The data to insert or update. + * @return Transaction The transaction, to enable method chaining. + */ + public function insertOrUpdateBatch($table, array $dataSet) + { + $this->enqueue(Operation::OP_INSERT_OR_UPDATE, $table, $dataSet); + + return $this; } /** * Enqueue an replace mutation. * + * Example: + * ``` + * $transaction->replace('Posts', [ + * 'ID' => 10, + * 'title' => 'My New Post [Replaced]', + * 'content' => 'Hello Moon' + * ]); + * ``` + * * @param string $table The table to replace into. * @param array $data The data to replace. - * @return void + * @return Transaction The transaction, to enable method chaining. */ public function replace($table, array $data) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } - - $this->mutations[] = $this->operation->mutation(Operation::OP_REPLACE, $table, $data); + return $this->replaceBatch($table, [$data]); } /** - * Enqueue an delete mutation. + * Enqueue one or more replace mutations. * - * @param string $table The table to delete from. - * @param array $key The key of the record to be deleted. - * @return void + * Example: + * ``` + * $transaction->replaceBatch('Posts', [ + * [ + * 'ID' => 10, + * 'title' => 'My New Post [Replaced]', + * 'content' => 'Hello Moon' + * ] + * ]); + * ``` + * + * @param string $table The table to replace into. + * @param array $dataSet The data to replace. + * @return Transaction The transaction, to enable method chaining. */ - public function delete($table, array $key) + public function replaceBatch($table, array $dataSet) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException( - 'Cannot perform mutations in a Read-Only Transaction' - ); - } + $this->enqueue(Operation::OP_REPLACE, $table, $dataSet); - $this->mutations[] = $this->operation->deleteMutation($table, $data); + return $this; } /** - * Run a query. + * Enqueue an delete mutation. + * + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [10] + * ]); * - * @param string $sql The query string to execute. - * @param array $options [optional] Configuration options. - * @return Result + * $transaction->delete('Posts', $keySet); + * ``` + * + * @param string $table The table to mutate. + * @param KeySet $keySet The KeySet to identify rows to delete. + * @return Transaction The transaction, to enable method chaining. */ - public function execute($sql, array $options = []) + public function delete($table, KeySet $keySet) { - return $this->operation->execute($this->session, $sql, [ - 'transactionId' => $this->transactionId - ] + $options); + $this->enqueue(Operation::OP_DELETE, $table, [$keySet]); + + return $this; } /** - * Lookup rows in a table. + * Roll back a transaction. * - * Note that if no KeySet is specified, all rows in a table will be - * returned. + * Rolls back a transaction, releasing any locks it holds. It is a good idea + * to call this for any transaction that includes one or more Read or + * ExecuteSql requests and ultimately decides not to commit. * - * @todo is returning everything a reasonable default? + * This closes the transaction, preventing any future API calls inside it. + * + * Rollback will NOT error if the transaction is not found or was already aborted. * - * @param string $table The table name. - * @param array $options [optional] { - * Configuration Options. + * Example: + * ``` + * $transaction->rollback(); + * ``` * - * @type string $index The name of an index on the table. - * @type array $columns A list of column names to be returned. - * @type array $keySet A [KeySet](https://cloud.google.com/spanner/reference/rest/v1/KeySet). - * @type int $offset The number of rows to offset results by. - * @type int $limit The number of results to return. - * } + * @param array $options [optional] Configuration Options. + * @return void */ - public function read($table, array $options = []) + public function rollback(array $options = []) { - return $this->operation->read($this->session, $table, [ - 'transactionId' => $this->transactionId - ] + $options); + if ($this->state !== self::STATE_ACTIVE) { + throw new \RuntimeException('The transaction cannot be rolled back because it is not active'); + } + + $this->state = self::STATE_ROLLED_BACK; + + return $this->operation->rollback($this->session, $this->transactionId, $options); } /** - * Commit all mutations in a transaction. + * Commit and end the transaction. * - * This closes the transaction, preventing any future API calls inside it. + * It is advised that transactions be run inside + * {@see Google\Cloud\Spanner\Database::runTransaction()} in order to take + * advantage of automated transaction retry in case of a transaction aborted + * error. + * + * Example: + * ``` + * $transaction->commit(); + * ``` * - * @codingStandardsIgnoreStart * @param array $options [optional] Configuration Options. - * @return array [Response Body](https://cloud.google.com/spanner/reference/rest/v1/projects.instances.databases.sessions/commit#response-body). - * @codingStandardsIgnoreEnd + * @return Timestamp The commit timestamp. + * @throws \RuntimeException If the transaction is not active + * @throws \AbortedException If the commit is aborted for any reason. */ public function commit(array $options = []) { - if ($this->context !== SessionPoolInterface::CONTEXT_READWRITE) { - throw new RuntimeException('Cannot commit in a Read-Only Transaction'); + if ($this->state !== self::STATE_ACTIVE) { + throw new \RuntimeException('The transaction cannot be committed because it is not active'); } - return $this->operation->commit($this->session, $this->mutations, [ - 'transactionId' => $this->transactionId - ] + $options); + $this->state = self::STATE_COMMITTED; + + $options['transactionId'] = $this->transactionId; + return $this->operation->commit($this->session, $this->mutations, $options); } /** - * Roll back a transaction. + * Retrieve the Transaction State. * - * Rolls back a transaction, releasing any locks it holds. It is a good idea - * to call this for any transaction that includes one or more Read or - * ExecuteSql requests and ultimately decides not to commit. + * Will be one of `Transaction::STATE_ACTIVE`, + * `Transaction::STATE_COMMITTED`, or `Transaction::STATE_ROLLED_BACK`. * - * This closes the transaction, preventing any future API calls inside it. + * Example: + * ``` + * $state = $transaction->state(); + * ``` * - * Rollback will NOT error if the transaction is not found or was already aborted. + * @return int + */ + public function state() + { + return $this->state; + } + + /** + * Format, validate and enqueue mutations in the transaction. * - * @param array $options [optional] Configuration Options. + * @param string $op The operation type. + * @param string $table The table name + * @param array $dataSet the mutations to enqueue * @return void */ - public function rollback(array $options = []) + private function enqueue($op, $table, array $dataSet) { - return $this->operation->rollback($this->session, $this->transactionId, $options); + foreach ($dataSet as $data) { + if ($op === Operation::OP_DELETE) { + $this->mutations[] = $this->operation->deleteMutation($table, $data); + } else { + $this->mutations[] = $this->operation->mutation($op, $table, $data); + } + } } } diff --git a/src/Spanner/TransactionConfigurationTrait.php b/src/Spanner/TransactionConfigurationTrait.php new file mode 100644 index 000000000000..12ff043713f9 --- /dev/null +++ b/src/Spanner/TransactionConfigurationTrait.php @@ -0,0 +1,152 @@ + false, + 'transactionType' => SessionPoolInterface::CONTEXT_READ, + 'transactionId' => null + ]; + + $type = null; + + $context = $this->pluck('transactionType', $options); + $id = $this->pluck('transactionId', $options); + if (!is_null($id)) { + $type = 'id'; + $transactionOptions = $id; + } elseif ($context === SessionPoolInterface::CONTEXT_READ) { + $transactionOptions = $this->configureSnapshotOptions($options); + } elseif ($context === SessionPoolInterface::CONTEXT_READWRITE) { + $transactionOptions = $this->configureTransactionOptions(); + } else { + throw new \BadMethodCallException(sprintf( + 'Invalid transaction context %s', + $context + )); + } + + $begin = $this->pluck('begin', $options); + if (is_null($type)) { + $type = ($begin) ? 'begin' : 'singleUse'; + } + + return [ + [$type => $transactionOptions], + $context + ]; + } + + private function configureTransactionOptions() + { + return [ + 'readWrite' => [] + ]; + } + + /** + * Create a Read Only single use transaction. + * + * @param array $options Configuration Options. + * @return array + */ + private function configureSnapshotOptions(array &$options) + { + $options += [ + 'returnReadTimestamp' => null, + 'strong' => null, + 'readTimestamp' => null, + 'exactStaleness' => null, + 'minReadTimestamp' => null, + 'maxStaleness' => null, + ]; + + $transactionOptions = [ + 'readOnly' => $this->arrayFilterRemoveNull([ + 'returnReadTimestamp' => $this->pluck('returnReadTimestamp', $options), + 'strong' => $this->pluck('strong', $options), + 'minReadTimestamp' => $this->pluck('minReadTimestamp', $options), + 'maxStaleness' => $this->pluck('maxStaleness', $options), + 'readTimestamp' => $this->pluck('readTimestamp', $options), + 'exactStaleness' => $this->pluck('exactStaleness', $options), + ]) + ]; + + if (empty($transactionOptions['readOnly'])) { + $transactionOptions['readOnly']['strong'] = true; + } + + $timestampFields = [ + 'minReadTimestamp', + 'readTimestamp' + ]; + + $durationFields = [ + 'exactStaleness', + 'maxStaleness' + ]; + + foreach ($timestampFields as $tsf) { + if (isset($transactionOptions['readOnly'][$tsf])) { + $field = $transactionOptions['readOnly'][$tsf]; + if (!($field instanceof Timestamp)) { + throw new \BadMethodCallException(sprintf( + 'Read Only Transaction Configuration Field %s must be an instance of Timestamp', + $tsf + )); + } + + $transactionOptions['readOnly'][$tsf] = $field->formatAsString(); + } + } + + foreach ($durationFields as $df) { + if (isset($transactionOptions['readOnly'][$df])) { + $field = $transactionOptions['readOnly'][$df]; + if (!($field instanceof Duration)) { + throw new \BadMethodCallException(sprintf( + 'Read Only Transaction Configuration Field %s must be an instance of Duration', + $df + )); + } + + $transactionOptions['readOnly'][$df] = $field->get(); + } + } + + return $transactionOptions; + } +} diff --git a/src/Spanner/TransactionReadTrait.php b/src/Spanner/TransactionReadTrait.php new file mode 100644 index 000000000000..44ba7d397e7e --- /dev/null +++ b/src/Spanner/TransactionReadTrait.php @@ -0,0 +1,138 @@ +execute( + * 'SELECT * FROM Users WHERE id = @userId', + * [ + * 'parameters' => [ + * 'userId' => 1 + * ] + * ] + * ); + * ``` + * @param string $sql The query string to execute. + * @param array $options [optional] { + * Configuration options. + * + * @type array $parameters A key/value array of Query Parameters, where + * the key is represented in the query string prefixed by a `@` + * symbol. + * } + * @return Result + */ + public function execute($sql, array $options = []) + { + $options['transactionType'] = $this->context; + $options['transactionId'] = $this->transactionId; + + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; + $options['transactionContext'] = $context; + + return $this->operation->execute($this->session, $sql, $options); + } + + /** + * Lookup rows in a table. + * + * Note that if no KeySet is specified, all rows in a table will be + * returned. + * + * Example: + * ``` + * $keySet = $spanner->keySet([ + * 'keys' => [10] + * ]); + * + * $result = $database->read('Posts', [ + * 'keySet' => $keySet + * ]); + * ``` + * + * @param string $table The table name. + * @param KeySet $keySet The KeySet to select rows. + * @param array $columns A list of column names to return. + * @param array $options [optional] { + * Configuration Options. + * + * @type string $index The name of an index on the table. + * @type int $offset The number of rows to offset results by. + * @type int $limit The number of results to return. + * } + * @return Result + */ + public function read($table, KeySet $keySet, array $columns, array $options = []) + { + $options['transactionType'] = $this->context; + $options['transactionId'] = $this->transactionId; + + list($transactionOptions, $context) = $this->transactionSelector($options); + $options['transaction'] = $transactionOptions; + $options['transactionContext'] = $context; + + return $this->operation->read($this->session, $table, $keySet, $columns, $options); + } + + /** + * Retrieve the Transaction ID. + * + * Example: + * ``` + * $id = $transaction->id(); + * ``` + * + * @return string + */ + public function id() + { + return $this->transactionId; + } +} diff --git a/src/Spanner/V1/SpannerClient.php b/src/Spanner/V1/SpannerClient.php index 45c6bbb66631..226893168443 100644 --- a/src/Spanner/V1/SpannerClient.php +++ b/src/Spanner/V1/SpannerClient.php @@ -1,16 +1,18 @@ createSession($formattedDatabase); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -96,8 +96,15 @@ class SpannerClient */ const DEFAULT_TIMEOUT_MILLIS = 30000; - const _CODEGEN_NAME = 'gapic'; - const _CODEGEN_VERSION = '0.1.0'; + /** + * The name of the code generator, to be included in the agent header. + */ + const CODEGEN_NAME = 'gapic'; + + /** + * The code generator version, to be included in the agent header. + */ + const CODEGEN_VERSION = '0.1.0'; private static $databaseNameTemplate; private static $sessionNameTemplate; @@ -216,12 +223,16 @@ private static function getSessionNameTemplate() return self::$sessionNameTemplate; } - private static function getPageStreamingDescriptors() + private static function getGrpcStreamingDescriptors() { - $pageStreamingDescriptors = [ + return [ + 'executeStreamingSql' => [ + 'grpcStreamingType' => 'ServerStreaming', + ], + 'streamingRead' => [ + 'grpcStreamingType' => 'ServerStreaming', + ], ]; - - return $pageStreamingDescriptors; } // TODO(garrettjones): add channel (when supported in gRPC) @@ -232,12 +243,12 @@ private static function getPageStreamingDescriptors() * Optional. Options for configuring the service API wrapper. * * @type string $serviceAddress The domain name of the API remote host. - * Default 'wrenchworks.googleapis.com'. + * Default 'spanner.googleapis.com'. * @type mixed $port The port on which to connect to the remote host. Default 443. - * @type Grpc\ChannelCredentials $sslCreds + * @type \Grpc\ChannelCredentials $sslCreds * A `ChannelCredentials` for use with an SSL-enabled channel. * Default: a credentials object returned from - * Grpc\ChannelCredentials::createSsl() + * \Grpc\ChannelCredentials::createSsl() * @type array $scopes A string array of scopes to use when acquiring credentials. * Default the scopes for the Google Cloud Spanner API. * @type array $retryingOverride @@ -252,21 +263,20 @@ private static function getPageStreamingDescriptors() * @type string $appName The codename of the calling service. Default 'gax'. * @type string $appVersion The version of the calling service. * Default: the current version of GAX. - * @type Google\Auth\CredentialsLoader $credentialsLoader + * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. * } */ public function __construct($options = []) { - $defaultScopes = [ - 'https://www.googleapis.com/auth/cloud-platform', - 'https://www.googleapis.com/auth/spanner.data', - ]; $defaultOptions = [ 'serviceAddress' => self::SERVICE_ADDRESS, 'port' => self::DEFAULT_SERVICE_PORT, - 'scopes' => $defaultScopes, + 'scopes' => [ + 'https://www.googleapis.com/auth/cloud-platform', + 'https://www.googleapis.com/auth/spanner.data', + ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, 'appName' => 'gax', @@ -277,8 +287,8 @@ public function __construct($options = []) $headerDescriptor = new AgentHeaderDescriptor([ 'clientName' => $options['appName'], 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::_CODEGEN_NAME, - 'codeGenVersion' => self::_CODEGEN_VERSION, + 'codeGenName' => self::CODEGEN_NAME, + 'codeGenVersion' => self::CODEGEN_VERSION, 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), 'phpVersion' => phpversion(), ]); @@ -289,14 +299,16 @@ public function __construct($options = []) 'getSession' => $defaultDescriptors, 'deleteSession' => $defaultDescriptors, 'executeSql' => $defaultDescriptors, + 'executeStreamingSql' => $defaultDescriptors, 'read' => $defaultDescriptors, + 'streamingRead' => $defaultDescriptors, 'beginTransaction' => $defaultDescriptors, 'commit' => $defaultDescriptors, 'rollback' => $defaultDescriptors, ]; - $pageStreamingDescriptors = self::getPageStreamingDescriptors(); - foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { - $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; + $grpcStreamingDescriptors = self::getGrpcStreamingDescriptors(); + foreach ($grpcStreamingDescriptors as $method => $grpcStreamingDescriptor) { + $this->descriptors[$method]['grpcStreamingDescriptor'] = $grpcStreamingDescriptor; } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/spanner_client_config.json'); @@ -322,6 +334,9 @@ public function __construct($options = []) $createSpannerStubFunction = function ($hostname, $opts) { return new SpannerGrpcClient($hostname, $opts); }; + if (array_key_exists('createSpannerStubFunction', $options)) { + $createSpannerStubFunction = $options['createSpannerStubFunction']; + } $this->spannerStub = $this->grpcCredentialsHelper->createStub( $createSpannerStubFunction, $options['serviceAddress'], @@ -359,9 +374,7 @@ public function __construct($options = []) * $formattedDatabase = SpannerClient::formatDatabaseName("[PROJECT]", "[INSTANCE]", "[DATABASE]"); * $response = $spannerClient->createSession($formattedDatabase); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -414,9 +427,7 @@ public function createSession($database, $optionalArgs = []) * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $response = $spannerClient->getSession($formattedName); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -467,9 +478,7 @@ public function getSession($name, $optionalArgs = []) * $formattedName = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); * $spannerClient->deleteSession($formattedName); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -529,9 +538,7 @@ public function deleteSession($name, $optionalArgs = []) * $sql = ""; * $response = $spannerClient->executeSql($formattedSession, $sql); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -628,6 +635,118 @@ public function executeSql($session, $sql, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * Like [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], except returns the result + * set as a stream. Unlike [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql], there + * is no limit on the size of the returned result set. However, no + * individual row in the result set can exceed 100 MiB, and no + * column value can exceed 10 MiB. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $sql = ""; + * // Read all responses until the stream is complete + * $stream = $spannerClient->executeStreamingSql($formattedSession, $sql); + * foreach ($stream->readAll() as $element) { + * // doSomethingWith($element); + * } + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the SQL query should be performed. + * @param string $sql Required. The SQL query string. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type Struct $params + * The SQL query string can contain parameter placeholders. A parameter + * placeholder consists of `'@'` followed by the parameter + * name. Parameter names consist of any combination of letters, + * numbers, and underscores. + * + * Parameters can appear anywhere that a literal value is expected. The same + * parameter name can be used more than once, for example: + * `"WHERE id > @msg_id AND id < @msg_id + 100"` + * + * It is an error to execute an SQL query with unbound parameters. + * + * Parameter values are specified using `params`, which is a JSON + * object whose keys are parameter names, and whose values are the + * corresponding parameter values. + * @type array $paramTypes + * It is not always possible for Cloud Spanner to infer the right SQL type + * from a JSON value. For example, values of type `BYTES` and values + * of type `STRING` both appear in [params][google.spanner.v1.ExecuteSqlRequest.params] as JSON strings. + * + * In these cases, `param_types` can be used to specify the exact + * SQL type for some or all of the SQL query parameters. See the + * definition of [Type][google.spanner.v1.Type] for more information + * about SQL types. + * @type string $resumeToken + * If this request is resuming a previously interrupted SQL query + * execution, `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new SQL query execution to resume where the last one left + * off. The rest of the request parameters must exactly match the + * request that yielded this token. + * @type QueryMode $queryMode + * Used to control the amount of debugging information returned in + * [ResultSetStats][google.spanner.v1.ResultSetStats]. + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * + * @return \Google\GAX\ServerStreamingResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function executeStreamingSql($session, $sql, $optionalArgs = []) + { + $request = new ExecuteSqlRequest(); + $request->setSession($session); + $request->setSql($sql); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['params'])) { + $request->setParams($optionalArgs['params']); + } + if (isset($optionalArgs['paramTypes'])) { + foreach ($optionalArgs['paramTypes'] as $key => $value) { + $request->addParamTypes((new ParamTypesEntry())->setKey($key)->setValue($value)); + } + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + if (isset($optionalArgs['queryMode'])) { + $request->setQueryMode($optionalArgs['queryMode']); + } + + $mergedSettings = $this->defaultCallSettings['executeStreamingSql']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'ExecuteStreamingSql', + $mergedSettings, + $this->descriptors['executeStreamingSql'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Reads rows from the database using key lookups and scans, as a * simple key/value style alternative to @@ -653,9 +772,7 @@ public function executeSql($session, $sql, $optionalArgs = []) * $keySet = new KeySet(); * $response = $spannerClient->read($formattedSession, $table, $columns, $keySet); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -743,6 +860,111 @@ public function read($session, $table, $columns, $keySet, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * Like [Read][google.spanner.v1.Spanner.Read], except returns the result set as a + * stream. Unlike [Read][google.spanner.v1.Spanner.Read], there is no limit on the + * size of the returned result set. However, no individual row in + * the result set can exceed 100 MiB, and no column value can exceed + * 10 MiB. + * + * Sample code: + * ``` + * try { + * $spannerClient = new SpannerClient(); + * $formattedSession = SpannerClient::formatSessionName("[PROJECT]", "[INSTANCE]", "[DATABASE]", "[SESSION]"); + * $table = ""; + * $columns = []; + * $keySet = new KeySet(); + * // Read all responses until the stream is complete + * $stream = $spannerClient->streamingRead($formattedSession, $table, $columns, $keySet); + * foreach ($stream->readAll() as $element) { + * // doSomethingWith($element); + * } + * } finally { + * $spannerClient->close(); + * } + * ``` + * + * @param string $session Required. The session in which the read should be performed. + * @param string $table Required. The name of the table in the database to be read. + * @param string[] $columns The columns of [table][google.spanner.v1.ReadRequest.table] to be returned for each row matching + * this request. + * @param KeySet $keySet Required. `key_set` identifies the rows to be yielded. `key_set` names the + * primary keys of the rows in [table][google.spanner.v1.ReadRequest.table] to be yielded, unless [index][google.spanner.v1.ReadRequest.index] + * is present. If [index][google.spanner.v1.ReadRequest.index] is present, then [key_set][google.spanner.v1.ReadRequest.key_set] instead names + * index keys in [index][google.spanner.v1.ReadRequest.index]. + * + * Rows are yielded in table primary key order (if [index][google.spanner.v1.ReadRequest.index] is empty) + * or index key order (if [index][google.spanner.v1.ReadRequest.index] is non-empty). + * + * It is not an error for the `key_set` to name rows that do not + * exist in the database. Read yields nothing for nonexistent rows. + * @param array $optionalArgs { + * Optional. + * + * @type TransactionSelector $transaction + * The transaction to use. If none is provided, the default is a + * temporary read-only transaction with strong concurrency. + * @type string $index + * If non-empty, the name of an index on [table][google.spanner.v1.ReadRequest.table]. This index is + * used instead of the table primary key when interpreting [key_set][google.spanner.v1.ReadRequest.key_set] + * and sorting result rows. See [key_set][google.spanner.v1.ReadRequest.key_set] for further information. + * @type int $limit + * If greater than zero, only the first `limit` rows are yielded. If `limit` + * is zero, the default is no limit. + * @type string $resumeToken + * If this request is resuming a previously interrupted read, + * `resume_token` should be copied from the last + * [PartialResultSet][google.spanner.v1.PartialResultSet] yielded before the interruption. Doing this + * enables the new read to resume where the last read left off. The + * rest of the request parameters must exactly match the request + * that yielded this token. + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * + * @return \Google\GAX\ServerStreamingResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function streamingRead($session, $table, $columns, $keySet, $optionalArgs = []) + { + $request = new ReadRequest(); + $request->setSession($session); + $request->setTable($table); + foreach ($columns as $elem) { + $request->addColumns($elem); + } + $request->setKeySet($keySet); + if (isset($optionalArgs['transaction'])) { + $request->setTransaction($optionalArgs['transaction']); + } + if (isset($optionalArgs['index'])) { + $request->setIndex($optionalArgs['index']); + } + if (isset($optionalArgs['limit'])) { + $request->setLimit($optionalArgs['limit']); + } + if (isset($optionalArgs['resumeToken'])) { + $request->setResumeToken($optionalArgs['resumeToken']); + } + + $mergedSettings = $this->defaultCallSettings['streamingRead']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->spannerStub, + 'StreamingRead', + $mergedSettings, + $this->descriptors['streamingRead'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Begins a new transaction. This step can often be skipped: * [Read][google.spanner.v1.Spanner.Read], [ExecuteSql][google.spanner.v1.Spanner.ExecuteSql] and @@ -757,9 +979,7 @@ public function read($session, $table, $columns, $keySet, $optionalArgs = []) * $options = new TransactionOptions(); * $response = $spannerClient->beginTransaction($formattedSession, $options); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -820,9 +1040,7 @@ public function beginTransaction($session, $options, $optionalArgs = []) * $mutations = []; * $response = $spannerClient->commit($formattedSession, $mutations); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * @@ -905,9 +1123,7 @@ public function commit($session, $mutations, $optionalArgs = []) * $transactionId = ""; * $spannerClient->rollback($formattedSession, $transactionId); * } finally { - * if (isset($spannerClient)) { - * $spannerClient->close(); - * } + * $spannerClient->close(); * } * ``` * diff --git a/src/Spanner/V1/resources/spanner_client_config.json b/src/Spanner/V1/resources/spanner_client_config.json index 6299ccfa6961..db4ced68c440 100644 --- a/src/Spanner/V1/resources/spanner_client_config.json +++ b/src/Spanner/V1/resources/spanner_client_config.json @@ -12,9 +12,9 @@ }, "retry_params": { "default": { - "initial_retry_delay_millis": 100, + "initial_retry_delay_millis": 1000, "retry_delay_multiplier": 1.3, - "max_retry_delay_millis": 60000, + "max_retry_delay_millis": 32000, "initial_rpc_timeout_millis": 60000, "rpc_timeout_multiplier": 1.0, "max_rpc_timeout_millis": 60000, diff --git a/src/Spanner/ValueInterface.php b/src/Spanner/ValueInterface.php new file mode 100644 index 000000000000..e5e140d1c751 --- /dev/null +++ b/src/Spanner/ValueInterface.php @@ -0,0 +1,44 @@ +returnInt64AsObject = $returnInt64AsObject; + } + + /** + * Accepts an array of key/value pairs, where the key is a SQL parameter + * name and the value is the value interpolated by the server, and returns + * an array of parameters and inferred parameter types. + * + * @param array $parameters The key/value parameters. + * @return array An associative array containing params and paramTypes. + */ + public function formatParamsForExecuteSql(array $parameters) + { + $paramTypes = []; + + foreach ($parameters as $key => $value) { + list ($parameters[$key], $paramTypes[$key]) = $this->paramType($value); + } + + return [ + 'params' => $parameters, + 'paramTypes' => $paramTypes + ]; + } + + /** + * Accepts a list of values and encodes the value into a format accepted by + * the Spanner API. + * + * @param array $values The list of values + * @return array The encoded values + */ + public function encodeValuesAsSimpleType(array $values) + { + $res = []; + foreach ($values as $value) { + $res[] = $this->paramType($value)[0]; + } + + return $res; + } + + /** + * Accepts a list of columns (with name and type) and a row from read or + * executeSql and decodes each value to its corresponding PHP type. + * + * @param array $columns The list of columns + * @param array $row The row data. + * @return array The decoded row data. + */ + public function decodeValues(array $columns, array $row, $extractResult = false) + { + $cols = []; + $types = []; + + foreach ($columns as $index => $column) { + $cols[] = (isset($column['name'])) + ? $column['name'] + : $index; + $types[] = $column['type']; + } + + $res = []; + foreach ($row as $index => $value) { + $i = $cols[$index]; + $res[$i] = $this->decodeValue($value, $types[$index]); + } + + return $res; + } + + /** + * Convert a timestamp string to a Timestamp class with nanosecond support. + * + * @param string $timestamp The timestamp string + * @return Timestamp + */ + public function createTimestampWithNanos($timestamp) + { + $matches = []; + preg_match(self::NANO_REGEX, $timestamp, $matches); + $timestamp = preg_replace(self::NANO_REGEX, '.000000Z', $timestamp); + + $dt = \DateTimeImmutable::createFromFormat(Timestamp::FORMAT, $timestamp); + return new Timestamp($dt, (isset($matches[1])) ? $matches[1] : 0); + } + + /** + * Convert a single value to its corresponding PHP type. + * + * @param mixed $value The value to decode + * @param array $type The value type + * @return mixed + */ + private function decodeValue($value, array $type) + { + switch ($type['code']) { + case self::TYPE_INT64: + $value = $this->returnInt64AsObject + ? new Int64($value) + : (int) $value; + break; + + case self::TYPE_TIMESTAMP: + $value = $this->createTimestampWithNanos($value); + break; + + case self::TYPE_DATE: + $value = new Date(new \DateTimeImmutable($value)); + break; + + case self::TYPE_BYTES: + $value = new Bytes(base64_decode($value)); + break; + + case self::TYPE_ARRAY: + $res = []; + foreach ($value as $item) { + $res[] = $this->decodeValue($item, $type['arrayElementType']); + } + + $value = $res; + break; + + case self::TYPE_STRUCT: + $value = $this->decodeValues($type['structType']['fields'], $value, true); + break; + + case self::TYPE_FLOAT64: + + // NaN, Infinite and -Infinite are possible FLOAT64 values, + // but when the gRPC response is decoded, they are represented + // as strings. This conditional checks for a string, converts to + // an equivalent double value, or dies if something really weird + // happens. + if (is_string($value)) { + switch ($value) { + case 'NaN': + $value = NAN; + break; + + case 'Infinity': + $value = INF; + break; + + case '-Infinity': + $value = -INF; + break; + + default: + throw new \RuntimeException(sprintf( + 'Unexpected string value %s encountered in FLOAT64 field.', + $value + )); + } + } + + break; + } + + return $value; + } + + /** + * Create a spanner parameter type value object from a PHP value type. + * + * @param mixed $value The PHP value + * @return array The Value type + */ + private function paramType($value) + { + $phpType = gettype($value); + switch ($phpType) { + case 'boolean': + $type = $this->typeObject(self::TYPE_BOOL); + break; + + case 'integer': + $value = (string) $value; + $type = $this->typeObject(self::TYPE_INT64); + break; + + case 'double': + $type = $this->typeObject(self::TYPE_FLOAT64); + break; + + case 'string': + $type = $this->typeObject(self::TYPE_STRING); + break; + + case 'resource': + $type = $this->typeObject(self::TYPE_BYTES); + $value = base64_encode(stream_get_contents($value)); + break; + + case 'object': + list ($type, $value) = $this->objectParam($value); + break; + + case 'array': + + if ($this->isAssoc($value)) { + throw new \InvalidArgumentException('Associative arrays are not supported'); + } + + $res = []; + $types = []; + foreach ($value as $element) { + $type = $this->paramType($element); + $res[] = $type[0]; + $types[] = $type[1]['code']; + } + + if (count(array_unique($types)) !== 1) { + throw new \InvalidArgumentException('Array values may not be of mixed type'); + } + + $type = $this->typeObject( + self::TYPE_ARRAY, + $this->typeObject($types[0]), + 'arrayElementType' + ); + + $value = $res; + break; + + case 'NULL': + $type = null; + break; + + default: + throw new \InvalidArgumentException(sprintf( + 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.', + $phpType + )); + break; + } + + return [$value, $type]; + } + + private function objectParam($value) + { + if ($value instanceof ValueInterface) { + return [ + $this->typeObject($value->type()), + $value->formatAsString() + ]; + } + + if ($value instanceof Int64) { + return [ + $this->typeObject(self::TYPE_INT64), + $value->get() + ]; + } + + throw new \InvalidArgumentException(sprintf( + 'Unrecognized value type %s. Please ensure you are using the latest version of google/cloud.', + get_class($value) + )); + } + + private function typeObject($type, array $nestedDefinition = [], $nestedDefinitionType = null) + { + return array_filter([ + 'code' => $type, + $nestedDefinitionType => $nestedDefinition + ]); + } +} diff --git a/tests/snippets/SpannerAdmin/ConfigurationTest.php b/tests/snippets/SpannerAdmin/ConfigurationTest.php new file mode 100644 index 000000000000..185a4b2b8e63 --- /dev/null +++ b/tests/snippets/SpannerAdmin/ConfigurationTest.php @@ -0,0 +1,119 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->config = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), + self::PROJECT, + self::CONFIG + ]); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Configuration::class); + $res = $snippet->invoke('configuration'); + + $this->assertInstanceOf(Configuration::class, $res->returnVal()); + $this->assertEquals(self::CONFIG, $res->returnVal()->name()); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'name'); + $snippet->addLocal('configuration', $this->config); + + $res = $snippet->invoke('name'); + $this->assertEquals(self::CONFIG, $res->returnVal()); + } + + public function testInfo() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'info'); + $snippet->addLocal('configuration', $this->config); + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]); + + $this->config->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals(self::CONFIG, $res->output()); + } + + public function testExists() + { + $snippet = $this->snippetFromMethod(Configuration::class, 'exists'); + $snippet->addLocal('configuration', $this->config); + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]); + + $this->config->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Configuration exists!', $res->output()); + } + + public function testReload() + { + $info = [ + 'name' => 'projects/'. self::PROJECT .'/instanceConfigs/'. self::CONFIG, + 'displayName' => self::CONFIG + ]; + + $snippet = $this->snippetFromMethod(Configuration::class, 'reload'); + $snippet->addLocal('configuration', $this->config); + + $this->connection->getConfig(Argument::any()) + ->shouldBeCalled() + ->willReturn($info); + + $this->config->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('info'); + $this->assertEquals($info, $res->returnVal()); + } +} diff --git a/tests/snippets/SpannerAdmin/DatabaseTest.php b/tests/snippets/SpannerAdmin/DatabaseTest.php new file mode 100644 index 000000000000..2aec4676511a --- /dev/null +++ b/tests/snippets/SpannerAdmin/DatabaseTest.php @@ -0,0 +1,147 @@ +prophesize(Instance::class); + $instance->name()->willReturn(self::INSTANCE); + + $this->connection = $this->prophesize(ConnectionInterface::class); + $this->database = \Google\Cloud\Dev\stub(Database::class, [ + $this->connection->reveal(), + $instance->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), + self::PROJECT, + self::DATABASE + ]); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Database::class, 'name'); + $snippet->addLocal('database', $this->database); + $res = $snippet->invoke('name'); + $this->assertEquals(self::DATABASE, $res->returnVal()); + } + + public function testExists() + { + $snippet = $this->snippetFromMethod(Database::class, 'exists'); + $snippet->addLocal('database', $this->database); + + $this->connection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willReturn(['statements' => []]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Database exists!', $res->output()); + } + + public function testUpdateDdl() + { + $snippet = $this->snippetFromMethod(Database::class, 'updateDdl'); + $snippet->addLocal('database', $this->database); + + $this->connection->updateDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $snippet->invoke(); + } + + public function testUpdateDdlBatch() + { + $snippet = $this->snippetFromMethod(Database::class, 'updateDdlBatch'); + $snippet->addLocal('database', $this->database); + + $this->connection->updateDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $snippet->invoke(); + } + + public function testDrop() + { + $snippet = $this->snippetFromMethod(Database::class, 'drop'); + $snippet->addLocal('database', $this->database); + + $this->connection->dropDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $snippet->invoke(); + } + + public function testDdl() + { + $snippet = $this->snippetFromMethod(Database::class, 'ddl'); + $snippet->addLocal('database', $this->database); + + $stmts = [ + 'CREATE TABLE TestSuites', + 'CREATE TABLE TestCases' + ]; + + $this->connection->getDatabaseDDL(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'statements' => $stmts + ]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('statements'); + $this->assertEquals($stmts, $res->returnVal()); + } + + public function testIam() + { + $snippet = $this->snippetFromMethod(Database::class, 'iam'); + $snippet->addLocal('database', $this->database); + + $res = $snippet->invoke('iam'); + $this->assertInstanceOf(Iam::class, $res->returnVal()); + } +} diff --git a/tests/snippets/SpannerAdmin/InstanceTest.php b/tests/snippets/SpannerAdmin/InstanceTest.php new file mode 100644 index 000000000000..1cfcf9ae6a24 --- /dev/null +++ b/tests/snippets/SpannerAdmin/InstanceTest.php @@ -0,0 +1,215 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->instance = \Google\Cloud\Dev\stub(Instance::class, [ + $this->connection->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), + self::PROJECT, + self::INSTANCE + ]); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Instance::class); + $res = $snippet->invoke('instance'); + $this->assertInstanceOf(Instance::class, $res->returnVal()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + } + + public function testName() + { + $snippet = $this->snippetFromMethod(Instance::class, 'name'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('name'); + $this->assertEquals(self::INSTANCE, $res->returnVal()); + } + + public function testInfo() + { + $snippet = $this->snippetFromMethod(Instance::class, 'info'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn(['nodeCount' => 1]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('1', $res->output()); + } + + public function testExists() + { + $snippet = $this->snippetFromMethod(Instance::class, 'exists'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn(['foo' => 'bar']); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Instance exists!', $res->output()); + } + + public function testReload() + { + $snippet = $this->snippetFromMethod(Instance::class, 'reload'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn(['nodeCount' => 1]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('info'); + $info = $this->instance->info(); + $this->assertEquals($info, $res->returnVal()); + } + + public function testState() + { + $snippet = $this->snippetFromMethod(Instance::class, 'state'); + $snippet->addLocal('instance', $this->instance); + $snippet->addUse(Instance::class); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn(['state' => Instance::STATE_READY]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke(); + $this->assertEquals('Instance is ready!', $res->output()); + } + + public function testUpdate() + { + $snippet = $this->snippetFromMethod(Instance::class, 'update'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->getInstance(Argument::any()) + ->shouldBeCalledTimes(1) + ->willReturn([ + 'displayName' => 'foo', + 'nodeCount' => 1 + ]); + + $this->connection->updateInstance(Argument::any()) + ->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + $snippet->invoke(); + } + + public function testDelete() + { + $snippet = $this->snippetFromMethod(Instance::class, 'delete'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->deleteInstance(Argument::any()) + ->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + $snippet->invoke(); + } + + public function testCreateDatabase() + { + $snippet = $this->snippetFromMethod(Instance::class, 'createDatabase'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->createDatabase(Argument::any()) + ->shouldBeCalled(); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + } + + public function testDatabase() + { + $snippet = $this->snippetFromMethod(Instance::class, 'database'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('database'); + $this->assertInstanceOf(Database::class, $res->returnVal()); + $this->assertEquals(self::DATABASE, $res->returnVal()->name()); + } + + public function databases() + { + $snippet = $this->snippetFromMethod(Instance::class, 'databases'); + $snippet->addLocal('instance', $this->instance); + + $this->connection->listDatabases(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'databases' => [ + 'projects/'. self::PROJECT .'/instances/'. self::INSTANCE .'/database/'. self::DATABASE + ] + ]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('databases'); + + $this->assertInstanceOf(\Generator::class, $res->returnVal()); + $this->assertInstanceOf(Database::class, $res->returnVal()->current()); + } + + public function testIam() + { + $snippet = $this->snippetFromMethod(Instance::class, 'iam'); + $snippet->addLocal('instance', $this->instance); + + $res = $snippet->invoke('iam'); + $this->assertInstanceOf(Iam::class, $res->returnVal()); + } +} diff --git a/tests/snippets/SpannerAdmin/SpannerClientTest.php b/tests/snippets/SpannerAdmin/SpannerClientTest.php new file mode 100644 index 000000000000..14e1a569d488 --- /dev/null +++ b/tests/snippets/SpannerAdmin/SpannerClientTest.php @@ -0,0 +1,129 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->client = \Google\Cloud\Dev\stub(SpannerClient::class); + $this->client->___setProperty('connection', $this->connection->reveal()); + } + + public function testConfigurations() + { + $this->connection->listConfigs(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instanceConfigs' => [ + ['name' => 'projects/my-awesome-projects/instanceConfigs/Foo'], + ['name' => 'projects/my-awesome-projects/instanceConfigs/Bar'], + ] + ]); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $snippet = $this->snippetFromMethod(SpannerClient::class, 'configurations'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('configurations'); + + $this->assertInstanceOf(\Generator::class, $res->returnVal()); + $this->assertInstanceOf(Configuration::class, $res->returnVal()->current()); + $this->assertEquals('Foo', $res->returnVal()->current()->name()); + } + + public function testConfiguration() + { + $configName = 'foo'; + + $snippet = $this->snippetFromMethod(SpannerClient::class, 'configuration'); + $snippet->addLocal('spanner', $this->client); + $snippet->addLocal('configurationName', self::CONFIG); + + $res = $snippet->invoke('configuration'); + $this->assertInstanceOf(Configuration::class, $res->returnVal()); + $this->assertEquals(self::CONFIG, $res->returnVal()->name()); + } + + public function testCreateInstance() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'createInstance'); + $snippet->addLocal('spanner', $this->client); + $snippet->addLocal('configuration', $this->client->configuration(self::CONFIG)); + + $this->connection->createInstance(Argument::any()) + ->shouldBeCalled() + ->willReturn([]); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('instance'); + $this->assertInstanceOf(Instance::class, $res->returnVal()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + } + + public function testInstance() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'instance'); + $snippet->addLocal('spanner', $this->client); + + $res = $snippet->invoke('instance'); + $this->assertInstanceOf(Instance::class, $res->returnVal()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->name()); + } + + public function testInstances() + { + $snippet = $this->snippetFromMethod(SpannerClient::class, 'instances'); + $snippet->addLocal('spanner', $this->client); + + $this->connection->listInstances(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'instances' => [ + ['name' => 'projects/my-awesome-project/instances/'. self::INSTANCE], + ['name' => 'projects/my-awesome-project/instances/Bar'] + ] + ]); + + $this->client->___setProperty('connection', $this->connection->reveal()); + + $res = $snippet->invoke('instances'); + $this->assertInstanceOf(\Generator::class, $res->returnVal()); + $this->assertInstanceOf(Instance::class, $res->returnVal()->current()); + $this->assertEquals(self::INSTANCE, $res->returnVal()->current()->name()); + } +} diff --git a/tests/snippets/bootstrap.php b/tests/snippets/bootstrap.php index af9eec686e44..9e4a5da3d639 100644 --- a/tests/snippets/bootstrap.php +++ b/tests/snippets/bootstrap.php @@ -35,7 +35,7 @@ function stub($name, $extends) { - $tpl = 'class %s extends %s {use \Google\Cloud\Dev\SetStubConnectionTrait; }'; + $tpl = 'class %s extends %s {use \Google\Cloud\Dev\StubTrait; }'; eval(sprintf($tpl, $name, $extends)); } diff --git a/tests/unit/ArrayTraitTest.php b/tests/unit/ArrayTraitTest.php index 53dc38667917..538490227988 100644 --- a/tests/unit/ArrayTraitTest.php +++ b/tests/unit/ArrayTraitTest.php @@ -83,6 +83,26 @@ public function testIsAssocFalse() $this->assertFalse($actual); } + + public function testArrayFilterRemoveNull() + { + $input = [ + 'null' => null, + 'false' => false, + 'zero' => 0, + 'float' => 0.0, + 'empty' => '', + 'array' => [], + ]; + + $res = $this->implementation->call('arrayFilterRemoveNull', [$input]); + $this->assertFalse(array_key_exists('null', $res)); + $this->assertTrue(array_key_exists('false', $res)); + $this->assertTrue(array_key_exists('zero', $res)); + $this->assertTrue(array_key_exists('float', $res)); + $this->assertTrue(array_key_exists('empty', $res)); + $this->assertTrue(array_key_exists('array', $res)); + } } class ArrayTraitStub diff --git a/tests/unit/PhpArrayTest.php b/tests/unit/PhpArrayTest.php index 4df80c571925..11188fdd7075 100644 --- a/tests/unit/PhpArrayTest.php +++ b/tests/unit/PhpArrayTest.php @@ -28,7 +28,7 @@ class PhpArrayTest extends \PHPUnit_Framework_TestCase { private function getCodec($customFilters = []) { - return new PhpArray($customFilters); + return new PhpArray(['customFilters' => $customFilters]); } /** diff --git a/tests/unit/Spanner/BytesTest.php b/tests/unit/Spanner/BytesTest.php new file mode 100644 index 000000000000..d224738f96e2 --- /dev/null +++ b/tests/unit/Spanner/BytesTest.php @@ -0,0 +1,52 @@ +content); + $this->assertEquals($this->content, $bytes->get()); + } + + public function testFormatAsString() + { + $bytes = new Bytes($this->content); + $this->assertEquals(base64_encode($this->content), $bytes->formatAsString()); + } + + public function testCast() + { + $bytes = new Bytes($this->content); + $this->assertEquals(base64_encode($this->content), (string) $bytes); + } + + public function testType() + { + $bytes = new Bytes($this->content); + $this->assertTrue(is_integer($bytes->type())); + } +} diff --git a/tests/unit/Spanner/DatabaseTest.php b/tests/unit/Spanner/DatabaseTest.php new file mode 100644 index 000000000000..567ab1dde2c3 --- /dev/null +++ b/tests/unit/Spanner/DatabaseTest.php @@ -0,0 +1,480 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->instance = $this->prophesize(Instance::class); + $this->sessionPool = $this->prophesize(SessionPoolInterface::class); + $this->sessionPool->session(self::INSTANCE, self::DATABASE, Argument::any()) + ->willReturn(new Session( + $this->connection->reveal(), + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + )); + + $this->instance->name()->willReturn(self::INSTANCE); + + $args = [ + $this->connection->reveal(), + $this->instance->reveal(), + $this->sessionPool->reveal(), + self::PROJECT, + self::DATABASE, + ]; + + $props = [ + 'connection', 'operation' + ]; + + $this->database = \Google\Cloud\Dev\stub(Database::class, $args, $props); + } + + public function testSnapshot() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $res = $this->database->snapshot(); + $this->assertInstanceOf(Snapshot::class, $res); + } + + /** + * @expectedException BadMethodCallException + */ + public function testSnapshotMinReadTimestamp() + { + $this->database->snapshot(['minReadTimestamp' => 'foo']); + } + + /** + * @expectedException BadMethodCallException + */ + public function testSnapshotMaxStaleness() + { + $this->database->snapshot(['maxStaleness' => 'foo']); + } + + public function testRunTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $hasTransaction = false; + + $this->database->runTransaction(function (Transaction $t) use (&$hasTransaction) { + $hasTransaction = true; + }); + + $this->assertTrue($hasTransaction); + } + + public function testRunTransactionRetry() + { + $abort = new AbortedException('foo', 409, null, [ + [ + 'retryDelay' => [ + 'seconds' => 1, + 'nanos' => 0 + ] + ] + ]); + + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalledTimes(3) + ->willReturn(['id' => self::TRANSACTION]); + + $it = 0; + $this->connection->commit(Argument::any()) + ->shouldBeCalledTimes(3) + ->will(function() use (&$it, $abort) { + $it++; + if ($it <= 2) { + throw $abort; + } + + return ['commitTimestamp' => TransactionTest::TIMESTAMP]; + }); + + $this->refreshOperation(); + + $this->database->runTransaction(function($t){$t->commit();}); + } + + /** + * @expectedException Google\Cloud\Exception\AbortedException + */ + public function testRunTransactionAborted() + { + $abort = new AbortedException('foo', 409, null, [ + [ + 'retryDelay' => [ + 'seconds' => 1, + 'nanos' => 0 + ] + ] + ]); + + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $it = 0; + $this->connection->commit(Argument::any()) + ->shouldBeCalled() + ->will(function() use (&$it, $abort) { + $it++; + if ($it <= 8) { + throw $abort; + } + + return ['commitTimestamp' => TransactionTest::TIMESTAMP]; + }); + + $this->refreshOperation(); + + $this->database->runTransaction(function($t){$t->commit();}); + } + + public function testTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->refreshOperation(); + + $t = $this->database->transaction(); + $this->assertInstanceOf(Transaction::class, $t); + } + + public function testInsert() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->insert($table, $row); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testInsertBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][OPERATION::OP_INSERT]['table'] !== $table) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][OPERATION::OP_INSERT]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->insertBatch($table, [$row]); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testUpdate() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->update($table, $row); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testUpdateBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->updateBatch($table, [$row]); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testInsertOrUpdate() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->insertOrUpdate($table, $row); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testInsertOrUpdateBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_INSERT_OR_UPDATE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->insertOrUpdateBatch($table, [$row]); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testReplace() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_REPLACE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->replace($table, $row); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testReplaceBatch() + { + $table = 'foo'; + $row = ['col' => 'val']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $row) { + if ($arg['mutations'][0][Operation::OP_REPLACE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['columns'][0] !== array_keys($row)[0]) return false; + if ($arg['mutations'][0][Operation::OP_REPLACE]['values'][0] !== current($row)) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->replaceBatch($table, [$row]); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testDelete() + { + $table = 'foo'; + $keys = [10, 'bar']; + + $this->connection->commit(Argument::that(function ($arg) use ($table, $keys) { + if ($arg['mutations'][0][Operation::OP_DELETE]['table'] !== $table) return false; + if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][0] !== (string) $keys[0]) return false; + if ($arg['mutations'][0][Operation::OP_DELETE]['keySet']['keys'][1] !== $keys[1]) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->commitResponse()); + + $this->refreshOperation(); + + $res = $this->database->delete($table, new KeySet(['keys' => $keys])); + $this->assertInstanceOf(Timestamp::class, $res); + $this->assertTimestampIsCorrect($res); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Table'; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { + if ($arg['sql'] !== $sql) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->database->execute($sql); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $table = 'Table'; + $opts = ['foo' => 'bar']; + + $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { + if ($arg['table'] !== $table) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['ID']) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->database->read($table, new KeySet(['all' => true]), ['ID']); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + // ******* + // Helpers + + private function refreshOperation() + { + $operation = new Operation($this->connection->reveal(), false); + $this->database->___setProperty('operation', $operation); + } + + private function commitResponse() + { + return ['commitTimestamp' => '2017-01-09T18:05:22.534799Z']; + } + + private function assertTimestampIsCorrect($res) + { + $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']); + + $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z')); + } +} diff --git a/tests/unit/Spanner/DateTest.php b/tests/unit/Spanner/DateTest.php new file mode 100644 index 000000000000..1c25c6bc3245 --- /dev/null +++ b/tests/unit/Spanner/DateTest.php @@ -0,0 +1,55 @@ +dt = new \DateTime('1989-10-11'); + $this->date = new Date($this->dt); + } + + public function testGet() + { + $this->assertEquals($this->dt, $this->date->get()); + } + + public function testFormatAsString() + { + $this->assertEquals($this->dt->format(Date::FORMAT), $this->date->formatAsString()); + } + + public function testCast() + { + $this->assertEquals($this->dt->format(Date::FORMAT), (string)$this->date); + } + + public function testType() + { + $this->assertTrue(is_integer($this->date->type())); + } +} diff --git a/tests/unit/Spanner/DurationTest.php b/tests/unit/Spanner/DurationTest.php new file mode 100644 index 000000000000..37d2575e42f7 --- /dev/null +++ b/tests/unit/Spanner/DurationTest.php @@ -0,0 +1,65 @@ +duration = new Duration(self::SECONDS, self::NANOS); + } + + public function testGet() + { + $this->assertEquals([ + 'seconds' => self::SECONDS, + 'nanos' => self::NANOS + ], $this->duration->get()); + } + + public function testType() + { + $this->assertEquals(Duration::TYPE, $this->duration->type()); + } + + public function testFormatAsString() + { + $this->assertEquals( + json_encode($this->duration->get()), + $this->duration->formatAsString() + ); + } + + public function testTostring() + { + $this->assertEquals( + json_encode($this->duration->get()), + (string)$this->duration + ); + } +} diff --git a/tests/unit/Spanner/KeyRangeTest.php b/tests/unit/Spanner/KeyRangeTest.php new file mode 100644 index 000000000000..c276ba677d5e --- /dev/null +++ b/tests/unit/Spanner/KeyRangeTest.php @@ -0,0 +1,95 @@ +range = new KeyRange; + } + + public function testGetters() + { + $range = new KeyRange([ + 'startType' => KeyRange::TYPE_CLOSED, + 'start' => ['foo'], + 'endType' => KeyRange::TYPE_OPEN, + 'end' => ['bar'] + ]); + + $this->assertEquals(['foo'], $range->start()); + $this->assertEquals(['bar'], $range->end()); + $this->assertEquals(['start' => KeyRange::TYPE_CLOSED, 'end' => KeyRange::TYPE_OPEN], $range->types()); + } + + public function testSetStart() + { + $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']); + $this->assertEquals(['foo'], $this->range->start()); + $this->assertEquals(KeyRange::TYPE_OPEN, $this->range->types()['start']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testSetStartInvalidType() + { + $this->range->setStart('foo', ['foo']); + } + + public function testSetEnd() + { + $this->range->setEnd(KeyRange::TYPE_OPEN, ['foo']); + $this->assertEquals(['foo'], $this->range->end()); + $this->assertEquals(KeyRange::TYPE_OPEN, $this->range->types()['end']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testSetEndInvalidType() + { + $this->range->setEnd('foo', ['foo']); + } + + public function testKeyRangeObject() + { + $this->range->setStart(KeyRange::TYPE_OPEN, ['foo']); + $this->range->setEnd(KeyRange::TYPE_CLOSED, ['bar']); + + $res = $this->range->keyRangeObject(); + + $this->assertEquals(['startOpen' => ['foo'], 'endClosed' => ['bar']], $res); + } + + /** + * @expectedException BadMethodCallException + */ + public function testKeyRangeObjectBadRange() + { + $this->range->keyRangeObject(); + } +} diff --git a/tests/unit/Spanner/KeySetTest.php b/tests/unit/Spanner/KeySetTest.php new file mode 100644 index 000000000000..75551a6dc946 --- /dev/null +++ b/tests/unit/Spanner/KeySetTest.php @@ -0,0 +1,119 @@ +prophesize(KeyRange::class); + $range->keyRangeObject()->willReturn('foo'); + + $set->addRange($range->reveal()); + + $this->assertEquals('foo', $set->keySetObject()['ranges'][0]); + } + + public function testSetRanges() + { + $set = new KeySet; + + $range1 = $this->prophesize(KeyRange::class); + $range1->keyRangeObject()->willReturn('foo'); + + $range2 = $this->prophesize(KeyRange::class); + $range2->keyRangeObject()->willReturn('bar'); + + $ranges = [ + $range1->reveal(), + $range2->reveal() + ]; + + $set->setRanges($ranges); + + $this->assertEquals('foo', $set->keySetObject()['ranges'][0]); + $this->assertEquals('bar', $set->keySetObject()['ranges'][1]); + } + + public function testAddKey() + { + $set = new KeySet; + + $key = 'key'; + + $set->addKey($key); + + $this->assertEquals($key, $set->keySetObject()['keys'][0]); + } + + public function testSetKeys() + { + $set = new KeySet; + + $keys = ['key1','key2']; + + $set->setKeys($keys); + + $this->assertEquals($keys, $set->keySetObject()['keys']); + } + + public function testSetMatchAll() + { + $set = new KeySet; + + $set->setMatchAll(true); + $this->assertTrue($set->keySetObject()['all']); + + $set->setMatchAll(false); + $this->assertFalse($set->keySetObject()['all']); + } + + public function testRanges() + { + $set = new KeySet; + $range = $this->prophesize(KeyRange::class)->reveal(); + + $set->addRange($range); + $this->assertEquals($range, $set->ranges()[0]); + } + + public function testKeys() + { + $set = new KeySet; + $key = 'foo'; + $set->addKey($key); + + $this->assertEquals($key, $set->keys()[0]); + } + + public function testMatchAll() + { + $set = new KeySet(); + $this->assertFalse($set->matchAll()); + + $set->setMatchAll(true); + $this->assertTrue($set->matchAll()); + } +} diff --git a/tests/unit/Spanner/OperationTest.php b/tests/unit/Spanner/OperationTest.php new file mode 100644 index 000000000000..298e8531632a --- /dev/null +++ b/tests/unit/Spanner/OperationTest.php @@ -0,0 +1,303 @@ +connection = $this->prophesize(ConnectionInterface::class); + + $this->operation = \Google\Cloud\Dev\stub(Operation::class, [ + $this->connection->reveal(), + false + ]); + + $session = $this->prophesize(Session::class); + $session->name()->willReturn(self::SESSION); + $this->session = $session->reveal(); + } + + public function testMutation() + { + $res = $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]); + + $this->assertEquals(Operation::OP_INSERT, array_keys($res)[0]); + $this->assertEquals('Posts', $res[Operation::OP_INSERT]['table']); + $this->assertEquals('foo', $res[Operation::OP_INSERT]['columns'][0]); + $this->assertEquals('bar', $res[Operation::OP_INSERT]['values'][0]); + } + + public function testDeleteMutation() + { + $keys = ['foo', 'bar']; + $range = new KeyRange([ + 'startType' => KeyRange::TYPE_CLOSED, + 'start' => ['foo'], + 'endType' => KeyRange::TYPE_OPEN, + 'end' => ['bar'] + ]); + + $keySet = new KeySet([ + 'keys' => $keys, + 'ranges' => [$range] + ]); + + $res = $this->operation->deleteMutation('Posts', $keySet); + + $this->assertEquals('Posts', $res['delete']['table']); + $this->assertEquals($keys, $res['delete']['keySet']['keys']); + $this->assertEquals($range->keyRangeObject(), $res['delete']['keySet']['ranges'][0]); + } + + public function testCommit() + { + $mutations = [ + $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]) + ]; + + $this->connection->commit(Argument::that(function ($arg) use ($mutations) { + if ($arg['mutations'] !== $mutations) return false; + if ($arg['transactionId'] !== 'foo') return false; + + return true; + }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->commit($this->session, $mutations, [ + 'transactionId' => 'foo' + ]); + + $this->assertInstanceOf(Timestamp::class, $res); + } + + public function testCommitWithExistingTransaction() + { + $mutations = [ + $this->operation->mutation(Operation::OP_INSERT, 'Posts', [ + 'foo' => 'bar' + ]) + ]; + + $this->connection->commit(Argument::that(function ($arg) use ($mutations) { + if ($arg['mutations'] !== $mutations) return false; + if (isset($arg['singleUseTransaction'])) return false; + if ($arg['transactionId'] !== self::TRANSACTION) return false; + + return true; + }))->shouldBeCalled()->willReturn(['commitTimestamp' => self::TIMESTAMP]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->commit($this->session, $mutations, [ + 'transactionId' => self::TRANSACTION + ]); + + $this->assertInstanceOf(Timestamp::class, $res); + } + + public function testRollback() + { + $this->connection->rollback(Argument::that(function ($arg) { + if ($arg['transactionId'] !== self::TRANSACTION) return false; + if ($arg['session'] !== self::SESSION) return false; + + return true; + }))->shouldBeCalled(); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $this->operation->rollback($this->session, self::TRANSACTION); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Posts WHERE ID = @id'; + $params = ['id' => 10]; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql, $params) { + if ($arg['sql'] !== $sql) return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['params'] !== ['id' => '10']) return false; + if ($arg['paramTypes']['id']['code'] !== ValueMapper::TYPE_INT64) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->execute($this->session, $sql, [ + 'parameters' => $params + ]); + + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse()); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo']); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testReadWithTransaction() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse([ + 'transaction' => ['id' => self::TRANSACTION] + ])); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ + 'transactionContext' => SessionPoolInterface::CONTEXT_READWRITE + ]); + $this->assertInstanceOf(Transaction::class, $res->transaction()); + $this->assertEquals(self::TRANSACTION, $res->transaction()->id()); + } + + public function testReadWithSnapshot() + { + $this->connection->read(Argument::that(function ($arg) { + if ($arg['table'] !== 'Posts') return false; + if ($arg['session'] !== self::SESSION) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['foo']) return false; + + return true; + }))->shouldBeCalled()->willReturn($this->executeAndReadResponse([ + 'transaction' => ['id' => self::TRANSACTION] + ])); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $res = $this->operation->read($this->session, 'Posts', new KeySet(['all' => true]), ['foo'], [ + 'transactionContext' => SessionPoolInterface::CONTEXT_READ + ]); + $this->assertInstanceOf(Snapshot::class, $res->snapshot()); + $this->assertEquals(self::TRANSACTION, $res->snapshot()->id()); + } + + public function testTransaction() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $t = $this->operation->transaction($this->session); + $this->assertInstanceOf(Transaction::class, $t); + $this->assertEquals(self::TRANSACTION, $t->id()); + } + + public function testSnapshot() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snap = $this->operation->snapshot($this->session); + $this->assertInstanceOf(Snapshot::class, $snap); + $this->assertEquals(self::TRANSACTION, $snap->id()); + } + + public function testSnapshotWithTimestamp() + { + $this->connection->beginTransaction(Argument::any()) + ->shouldBeCalled() + ->willReturn(['id' => self::TRANSACTION, 'readTimestamp' => self::TIMESTAMP]); + + $this->operation->___setProperty('connection', $this->connection->reveal()); + + $snap = $this->operation->snapshot($this->session); + $this->assertInstanceOf(Snapshot::class, $snap); + $this->assertEquals(self::TRANSACTION, $snap->id()); + $this->assertInstanceOf(Timestamp::class, $snap->readTimestamp()); + } + + private function executeAndReadResponse(array $additionalMetadata = []) + { + return [ + 'metadata' => array_merge([ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], $additionalMetadata), + 'rows' => [ + ['10'] + ] + ]; + } +} diff --git a/tests/unit/Spanner/ResultTest.php b/tests/unit/Spanner/ResultTest.php new file mode 100644 index 000000000000..98fa6d8ef6b5 --- /dev/null +++ b/tests/unit/Spanner/ResultTest.php @@ -0,0 +1,106 @@ + 'John'] + ]); + + $res = iterator_to_array($result); + $this->assertEquals(1, count($res)); + $this->assertEquals('John', $res[0]['name']); + } + + public function testMetadata() + { + $result = new Result(['metadata' => 'foo'], []); + $this->assertEquals('foo', $result->metadata()); + } + + public function testRows() + { + $rows = [ + ['name' => 'John'] + ]; + + $result = new Result([], $rows); + + $this->assertEquals($rows, $result->rows()); + } + + public function testFirstRow() + { + $rows = [ + ['name' => 'John'], + ['name' => 'Dave'] + ]; + + $result = new Result([], $rows); + + $this->assertEquals($rows[0], $result->firstRow()); + } + + public function testStats() + { + $result = new Result(['stats' => 'foo'], []); + $this->assertEquals('foo', $result->stats()); + } + + public function testInfo() + { + $info = ['foo' => 'bar']; + $result = new Result($info, []); + + $this->assertEquals($info, $result->info()); + } + + public function testTransaction() + { + $result = new Result([], [], [ + 'transaction' => 'foo' + ]); + + $this->assertEquals('foo', $result->transaction()); + + $result = new Result([], []); + + $this->assertNull($result->transaction()); + } + + public function testSnapshot() + { + $result = new Result([], [], [ + 'snapshot' => 'foo' + ]); + + $this->assertEquals('foo', $result->snapshot()); + + $result = new Result([], []); + + $this->assertNull($result->snapshot()); + } +} diff --git a/tests/unit/Spanner/SnapshotTest.php b/tests/unit/Spanner/SnapshotTest.php new file mode 100644 index 000000000000..cf54179ce9ed --- /dev/null +++ b/tests/unit/Spanner/SnapshotTest.php @@ -0,0 +1,48 @@ +timestamp = new Timestamp(new \DateTime); + $this->snapshot = new Snapshot( + $this->prophesize(Operation::class)->reveal(), + $this->prophesize(Session::class)->reveal(), + 'foo', + $this->timestamp + ); + } + + public function testReadTimestamp() + { + $this->assertEquals($this->timestamp, $this->snapshot->readTimestamp()); + } +} diff --git a/tests/unit/Spanner/SpannerClientTest.php b/tests/unit/Spanner/SpannerClientTest.php new file mode 100644 index 000000000000..ca423786b858 --- /dev/null +++ b/tests/unit/Spanner/SpannerClientTest.php @@ -0,0 +1,112 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->client = \Google\Cloud\Dev\stub(SpannerClient::class); + } + + public function testConnect() + { + $database = $this->client->connect(self::INSTANCE, self::DATABASE); + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals(self::DATABASE, $database->name()); + } + + public function testConnectWithInstance() + { + $inst = $this->client->instance(self::INSTANCE); + $database = $this->client->connect($inst, self::DATABASE); + $this->assertInstanceOf(Database::class, $database); + $this->assertEquals(self::DATABASE, $database->name()); + } + + public function testKeyset() + { + $ks = $this->client->keySet(); + $this->assertInstanceOf(KeySet::class, $ks); + } + + public function testKeyRange() + { + $kr = $this->client->keyRange(); + $this->assertInstanceOf(KeyRange::class, $kr); + } + + public function testBytes() + { + $b = $this->client->bytes('foo'); + $this->assertInstanceOf(Bytes::class, $b); + $this->assertEquals(base64_encode('foo'), (string)$b); + } + + public function testDate() + { + $d = $this->client->date(new \DateTime); + $this->assertInstanceOf(Date::class, $d); + } + + public function testTimestamp() + { + $ts = $this->client->timestamp(new \DateTime); + $this->assertInstanceOf(Timestamp::class, $ts); + } + + public function testInt64() + { + $i64 = $this->client->int64('123'); + $this->assertInstanceOf(Int64::class, $i64); + } + + public function testDuration() + { + $d = $this->client->duration(10, 1); + $this->assertInstanceOf(Duration::class, $d); + } + + public function testSessionClient() + { + $sc = $this->client->sessionClient(); + $this->assertInstanceOf(SessionClient::class, $sc); + } +} diff --git a/tests/unit/Spanner/TimestampTest.php b/tests/unit/Spanner/TimestampTest.php new file mode 100644 index 000000000000..c18ee94e95c2 --- /dev/null +++ b/tests/unit/Spanner/TimestampTest.php @@ -0,0 +1,61 @@ +dt = new \DateTime('1989-10-11 08:58:00 +00:00'); + $this->ts = new Timestamp($this->dt); + } + + public function testGet() + { + $this->assertEquals($this->dt, $this->ts->get()); + } + + public function testFormatAsString() + { + $this->assertEquals( + (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'), + (new \DateTime($this->ts->formatAsString()))->format('U') + ); + } + + public function testCast() + { + $this->assertEquals( + (new \DateTime($this->dt->format(Timestamp::FORMAT)))->format('U'), + (new \DateTime((string)$this->ts))->format('U') + ); + } + + public function testType() + { + $this->assertTrue(is_integer($this->ts->type())); + } +} diff --git a/tests/unit/Spanner/TransactionConfigurationTraitTest.php b/tests/unit/Spanner/TransactionConfigurationTraitTest.php new file mode 100644 index 000000000000..2dd3f408e0b3 --- /dev/null +++ b/tests/unit/Spanner/TransactionConfigurationTraitTest.php @@ -0,0 +1,185 @@ +impl = new TransactionConfigurationTraitImplementation; + $this->ts = new Timestamp(new \DateTime(self::TIMESTAMP), self::NANOS); + $this->duration = new Duration(10,1); + $this->dur = ['seconds' => 10, 'nanos' => 1]; + } + + public function testTransactionSelectorBasicSnapshot() + { + $args = []; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertTrue($res[0]['singleUse']['readOnly']['strong']); + } + + public function testTransactionSelectorExistingId() + { + $args = ['transactionId' => self::TRANSACTION]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertEquals(self::TRANSACTION, $res[0]['id']); + } + + public function testTransactionSelectorReadWrite() + { + $args = ['transactionType' => SessionPoolInterface::CONTEXT_READWRITE]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READWRITE, $res[1]); + $this->assertEquals($this->impl->proxyConfigureTransactionOptions(), $res[0]['singleUse']); + } + + public function testBegin() + { + $args = ['begin' => true]; + $res = $this->impl->proxyTransactionSelector($args); + $this->assertEquals(SessionPoolInterface::CONTEXT_READ, $res[1]); + $this->assertTrue($res[0]['begin']['readOnly']['strong']); + } + + public function testConfigureSnapshotOptionsReturnReadTimestamp() + { + $args = ['returnReadTimestamp' => true]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertTrue($res['readOnly']['returnReadTimestamp']); + } + + public function testConfigureSnapshotOptionsStrong() + { + $args = ['strong' => true]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertTrue($res['readOnly']['strong']); + } + + public function testConfigureSnapshotOptionsMinReadTimestamp() + { + $args = ['minReadTimestamp' => $this->ts]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals(self::TIMESTAMP, $res['readOnly']['minReadTimestamp']); + } + + public function testConfigureSnapshotOptionsReadTimestamp() + { + $args = ['readTimestamp' => $this->ts]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals(self::TIMESTAMP, $res['readOnly']['readTimestamp']); + } + + public function testConfigureSnapshotOptionsMaxStaleness() + { + $args = ['maxStaleness' => $this->duration]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals($this->dur, $res['readOnly']['maxStaleness']); + } + + public function testConfigureSnapshotOptionsExactStaleness() + { + $args = ['exactStaleness' => $this->duration]; + $res = $this->impl->proxyConfigureSnapshotOptions($args); + $this->assertEquals($this->dur, $res['readOnly']['exactStaleness']); + } + + /** + * @expectedException BadMethodCallException + */ + public function testTransactionSelectorInvalidContext() + { + $args = ['transactionType' => 'foo']; + $this->impl->proxyTransactionSelector($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidExactStaleness() + { + $args = ['exactStaleness' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidMaxStaleness() + { + $args = ['maxStaleness' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidMinReadTimestamp() + { + $args = ['minReadTimestamp' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } + + /** + * @expectedException BadMethodCallException + */ + public function testConfigureSnapshotOptionsInvalidReadTimestamp() + { + $args = ['readTimestamp' => 'foo']; + $this->impl->proxyConfigureSnapshotOptions($args); + } +} + +class TransactionConfigurationTraitImplementation +{ + use TransactionConfigurationTrait; + + public function proxyTransactionSelector(array &$options) + { + return $this->transactionSelector($options); + } + + public function proxyConfigureTransactionOptions() + { + return $this->configureTransactionOptions(); + } + + public function proxyConfigureSnapshotOptions(array &$options) + { + return $this->configureSnapshotOptions($options); + } +} diff --git a/tests/unit/Spanner/TransactionTest.php b/tests/unit/Spanner/TransactionTest.php new file mode 100644 index 000000000000..bb5d52151b23 --- /dev/null +++ b/tests/unit/Spanner/TransactionTest.php @@ -0,0 +1,331 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->operation = new Operation($this->connection->reveal(), false); + + $this->session = new Session( + $this->connection->reveal(), + self::PROJECT, + self::INSTANCE, + self::DATABASE, + self::SESSION + ); + + $args = [ + $this->operation, + $this->session, + self::TRANSACTION, + ]; + + $props = [ + 'operation', 'readTimestamp', 'state' + ]; + + $this->transaction = \Google\Cloud\Dev\stub(Transaction::class, $args, $props); + } + + public function testInsert() + { + $this->transaction->insert('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + } + + public function testInsertBatch() + { + $this->transaction->insertBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insert']['table']); + $this->assertEquals('foo', $mutations[0]['insert']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insert']['values'][0]); + } + + public function testUpdate() + { + $this->transaction->update('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['update']['table']); + $this->assertEquals('foo', $mutations[0]['update']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['update']['values'][0]); + } + + public function testUpdateBatch() + { + $this->transaction->updateBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['update']['table']); + $this->assertEquals('foo', $mutations[0]['update']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['update']['values'][0]); + } + + public function testInsertOrUpdate() + { + $this->transaction->insertOrUpdate('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insertOrUpdate']['table']); + $this->assertEquals('foo', $mutations[0]['insertOrUpdate']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insertOrUpdate']['values'][0]); + } + + public function testInsertOrUpdateBatch() + { + $this->transaction->insertOrUpdateBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['insertOrUpdate']['table']); + $this->assertEquals('foo', $mutations[0]['insertOrUpdate']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['insertOrUpdate']['values'][0]); + } + + public function testReplace() + { + $this->transaction->replace('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['replace']['table']); + $this->assertEquals('foo', $mutations[0]['replace']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['replace']['values'][0]); + } + + public function testReplaceBatch() + { + $this->transaction->replaceBatch('Posts', [['foo' => 'bar']]); + + $mutations = $this->transaction->___getProperty('mutations'); + + $this->assertEquals('Posts', $mutations[0]['replace']['table']); + $this->assertEquals('foo', $mutations[0]['replace']['columns'][0]); + $this->assertEquals('bar', $mutations[0]['replace']['values'][0]); + } + + public function testDelete() + { + $this->transaction->delete('Posts', new KeySet(['keys' => ['foo']])); + + $mutations = $this->transaction->___getProperty('mutations'); + $this->assertEquals('Posts', $mutations[0]['delete']['table']); + $this->assertEquals('foo', $mutations[0]['delete']['keySet']['keys'][0]); + $this->assertFalse($mutations[0]['delete']['keySet']['all']); + } + + public function testExecute() + { + $sql = 'SELECT * FROM Table'; + + $this->connection->executeSql(Argument::that(function ($arg) use ($sql) { + if ($arg['transaction']['id'] !== self::TRANSACTION) return false; + if ($arg['sql'] !== $sql) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->transaction->execute($sql); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testRead() + { + $table = 'Table'; + $opts = ['foo' => 'bar']; + + $this->connection->read(Argument::that(function ($arg) use ($table, $opts) { + if ($arg['transaction']['id'] !== self::TRANSACTION) return false; + if ($arg['table'] !== $table) return false; + if ($arg['keySet']['all'] !== true) return false; + if ($arg['columns'] !== ['ID']) return false; + + return true; + }))->shouldBeCalled()->willReturn([ + 'metadata' => [ + 'rowType' => [ + 'fields' => [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64 + ] + ] + ] + ] + ], + 'rows' => [ + [ + '10' + ] + ] + ]); + + $this->refreshOperation(); + + $res = $this->transaction->read($table, new KeySet(['all' => true]), ['ID']); + $this->assertInstanceOf(Result::class, $res); + $this->assertEquals(10, $res->rows()[0]['ID']); + } + + public function testCommit() + { + $this->transaction->insert('Posts', ['foo' => 'bar']); + + $mutations = $this->transaction->___getProperty('mutations'); + + $operation = $this->prophesize(Operation::class); + $operation->commit($this->session, $mutations, ['transactionId' => self::TRANSACTION])->shouldBeCalled(); + + $this->transaction->___setProperty('operation', $operation->reveal()); + + $this->transaction->commit(); + } + + /** + * @expectedException RuntimeException + */ + public function testCommitInvalidState() + { + $this->transaction->___setProperty('state', 'foo'); + $this->transaction->commit(); + } + + public function testRollback() + { + $this->connection->rollback(Argument::any()) + ->shouldBeCalled(); + + $this->refreshOperation(); + + $this->transaction->rollback(); + } + + /** + * @expectedException RuntimeException + */ + public function testRollbackInvalidState() + { + $this->transaction->___setProperty('state', 'foo'); + $this->transaction->rollback(); + } + + public function testId() + { + $this->assertEquals(self::TRANSACTION, $this->transaction->id()); + } + + public function testState() + { + $this->assertEquals(Transaction::STATE_ACTIVE, $this->transaction->state()); + + $this->transaction->___setProperty('state', Transaction::STATE_COMMITTED); + $this->assertEquals(Transaction::STATE_COMMITTED, $this->transaction->state()); + } + + public function testMutations() + { + $this->assertEmpty($this->transaction->mutations()); + $this->transaction->insert('Posts', []); + $this->assertEquals(1, count($this->transaction->mutations())); + } + + // ******* + // Helpers + + private function refreshOperation() + { + $operation = new Operation($this->connection->reveal(), false); + $this->transaction->___setProperty('operation', $operation); + } + + private function commitResponse() + { + return ['commitTimestamp' => self::TIMESTAMP]; + } + + private function assertTimestampIsCorrect($res) + { + $ts = new \DateTimeImmutable($this->commitResponse()['commitTimestamp']); + + $this->assertEquals($ts->format('Y-m-d\TH:i:s\Z'), $res->get()->format('Y-m-d\TH:i:s\Z')); + } +} diff --git a/tests/unit/Spanner/ValueMapperTest.php b/tests/unit/Spanner/ValueMapperTest.php new file mode 100644 index 000000000000..11840d40cd0d --- /dev/null +++ b/tests/unit/Spanner/ValueMapperTest.php @@ -0,0 +1,385 @@ +mapper = new ValueMapper(false); + } + + public function testFormatParamsForExecuteSqlSimpleTypes() + { + $params = [ + 'id' => 1, + 'name' => 'john', + 'pi' => 3.1515, + 'isCool' => false, + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($params, $res['params']); + $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['id']['code']); + $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['name']['code']); + $this->assertEquals(ValueMapper::TYPE_FLOAT64, $res['paramTypes']['pi']['code']); + $this->assertEquals(ValueMapper::TYPE_BOOL, $res['paramTypes']['isCool']['code']); + } + + public function testFormatParamsForExecuteSqlResource() + { + $c = 'hello world'; + + $resource = fopen('php://temp', 'r+'); + fwrite($resource, $c); + rewind($resource); + + $params = [ + 'resource' => $resource + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($c, base64_decode($res['params']['resource'])); + $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['resource']['code']); + } + + public function testFormatParamsForExecuteSqlArray() + { + $params = [ + 'array' => ['foo', 'bar'] + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals('foo', $res['params']['array'][0]); + $this->assertEquals('bar', $res['params']['array'][1]); + $this->assertEquals(ValueMapper::TYPE_ARRAY, $res['paramTypes']['array']['code']); + $this->assertEquals(ValueMapper::TYPE_STRING, $res['paramTypes']['array']['arrayElementType']['code']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlArrayInvalidAssoc() + { + $this->mapper->formatParamsForExecuteSql(['array' => [ + 'foo' => 'bar' + ]]); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlInvalidTypes() + { + $this->mapper->formatParamsForExecuteSql(['array' => ['foo', 3.1515]]); + } + + public function testFormatParamsForExecuteSqlInt64() + { + $val = '1234'; + $params = [ + 'int' => new Int64($val) + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + + $this->assertEquals($val, $res['params']['int']); + $this->assertEquals(ValueMapper::TYPE_INT64, $res['paramTypes']['int']['code']); + } + + public function testFormatParamsForExecuteSqlValueInterface() + { + $val = 'hello world'; + $params = [ + 'bytes' => new Bytes($val) + ]; + + $res = $this->mapper->formatParamsForExecuteSql($params); + $this->assertEquals($val, base64_decode($res['params']['bytes'])); + $this->assertEquals(ValueMapper::TYPE_BYTES, $res['paramTypes']['bytes']['code']); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testFormatParamsForExecuteSqlInvalidObjectType() + { + $params = [ + 'bad' => $this + ]; + + $this->mapper->formatParamsForExecuteSql($params); + } + + public function testEncodeValuesAsSimpleType() + { + $dt = new \DateTime; + + $vals = []; + $vals['bool'] = true; + $vals['int'] = 555555; + $vals['intObj'] = new Int64((string) $vals['int']); + $vals['float'] = 3.1415; + $vals['nan'] = NAN; + $vals['inf'] = INF; + $vals['timestamp'] = new Timestamp($dt); + $vals['date'] = new Date($dt); + $vals['string'] = 'foo'; + $vals['bytes'] = new Bytes('hello world'); + $vals['array'] = ['foo', 'bar']; + + $res = $this->mapper->encodeValuesAsSimpleType($vals); + + $this->assertTrue($res[0]); + $this->assertEquals((string) $vals['int'], $res[1]); + $this->assertEquals((string) $vals['int'], $res[2]); + $this->assertEquals($vals['float'], $res[3]); + $this->assertTrue(is_nan($res[4])); + $this->assertEquals(INF, $res[5]); + $this->assertEquals($dt->format(Timestamp::FORMAT), $res[6]); + $this->assertEquals($dt->format(Date::FORMAT), $res[7]); + $this->assertEquals($vals['string'], $res[8]); + $this->assertEquals(base64_encode('hello world'), $res[9]); + $this->assertEquals($vals['array'], $res[10]); + } + + public function testDecodeValuesBool() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_BOOL), + $this->createRow(false) + ); + $this->assertEquals(false, $res['rowName']); + } + + public function testDecodeValuesInt() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_INT64), + $this->createRow('555') + ); + $this->assertEquals(555, $res['rowName']); + } + + public function testDecodeValuesInt64Object() + { + $mapper = new ValueMapper(true); + $res = $mapper->decodeValues( + $this->createField(ValueMapper::TYPE_INT64), + $this->createRow('555') + ); + $this->assertInstanceOf(Int64::class, $res['rowName']); + $this->assertEquals('555', $res['rowName']->get()); + } + + public function testDecodeValuesFloat() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow(3.1415) + ); + $this->assertEquals(3.1415, $res['rowName']); + } + + public function testDecodeValuesFloatNaN() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('NaN') + ); + $this->assertTrue(is_nan($res['rowName'])); + } + + public function testDecodeValuesFloatInfinity() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('Infinity') + ); + + $this->assertTrue(is_infinite($res['rowName'])); + $this->assertTrue($res['rowName'] > 0); + } + + public function testDecodeValuesFloatNegativeInfinity() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('-Infinity') + ); + + $this->assertTrue(is_infinite($res['rowName'])); + $this->assertTrue($res['rowName'] < 0); + } + + /** + * @expectedException RuntimeException + */ + public function testDecodeValuesFloatError() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_FLOAT64), + $this->createRow('foo') + ); + } + + public function testDecodeValuesString() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_STRING), + $this->createRow('foo') + ); + $this->assertEquals('foo', $res['rowName']); + } + + public function testDecodeValuesTimestamp() + { + $dt = new \DateTime; + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_TIMESTAMP), + $this->createRow($dt->format(Timestamp::FORMAT)) + ); + + $this->assertInstanceOf(Timestamp::class, $res['rowName']); + $this->assertEquals($dt->format(Timestamp::FORMAT), $res['rowName']->formatAsString()); + } + + public function testDecodeValuesDate() + { + $dt = new \DateTime; + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_DATE), + $this->createRow($dt->format(Date::FORMAT)) + ); + + $this->assertInstanceOf(Date::class, $res['rowName']); + $this->assertEquals($dt->format(Date::FORMAT), $res['rowName']->formatAsString()); + } + + public function testDecodeValuesBytes() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_BYTES), + $this->createRow(base64_encode('hello world')) + ); + + $this->assertInstanceOf(Bytes::class, $res['rowName']); + $this->assertEquals('hello world', $res['rowName']->get()); + } + + public function testDecodeValuesArray() + { + $res = $this->mapper->decodeValues( + $this->createField(ValueMapper::TYPE_ARRAY, 'arrayElementType', [ + 'code' => ValueMapper::TYPE_STRING + ]), $this->createRow(['foo', 'bar']) + ); + + $this->assertEquals('foo', $res['rowName'][0]); + $this->assertEquals('bar', $res['rowName'][1]); + } + + public function testDecodeValuesStruct() + { + $field = [ + 'name' => 'structTest', + 'type' => [ + 'code' => ValueMapper::TYPE_ARRAY, + 'arrayElementType' => [ + 'code' => ValueMapper::TYPE_STRUCT, + 'structType' => [ + 'fields' => [ + [ + 'name' => 'rowName', + 'type' => [ + 'code' => ValueMapper::TYPE_STRING + ] + ] + ] + ] + ] + ] + ]; + + $row = [ + [ + 'Hello World' + ] + ]; + + $res = $this->mapper->decodeValues( + [$field], + [$row] + ); + + $this->assertEquals('Hello World', $res['structTest'][0]['rowName']); + } + + public function testDecodeValuesAnonymousField() + { + $fields = [ + [ + 'name' => 'ID', + 'type' => [ + 'code' => ValueMapper::TYPE_INT64, + ] + ], [ + 'type' => [ + 'code' => ValueMapper::TYPE_STRING + ] + ] + ]; + + $row = ['1337', 'John']; + + $res = $this->mapper->decodeValues($fields, $row); + + $this->assertEquals('1337', $res['ID']); + $this->assertEquals('John', $res[1]); + } + + private function createField($code, $type = null, array $typeObj = []) + { + return [[ + 'name' => 'rowName', + 'type' => array_filter([ + 'code' => $code, + $type => $typeObj + ]) + ]]; + } + + private function createRow($val) + { + return [$val]; + } +} diff --git a/tests/Spanner/ConfigurationTest.php b/tests/unit/SpannerAdmin/ConfigurationTest.php similarity index 63% rename from tests/Spanner/ConfigurationTest.php rename to tests/unit/SpannerAdmin/ConfigurationTest.php index 477e36fb70e2..360ba9e5e1c0 100644 --- a/tests/Spanner/ConfigurationTest.php +++ b/tests/unit/SpannerAdmin/ConfigurationTest.php @@ -15,33 +15,33 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\SpannerAdmin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Configuration; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class ConfigurationTest extends \PHPUnit_Framework_TestCase { const PROJECT_ID = 'test-project'; const NAME = 'test-config'; - private $adminConnection; + private $connection; private $configuration; public function setUp() { - $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); - $this->configuration = new ConfigurationStub( - $this->adminConnection->reveal(), + $this->connection = $this->prophesize(ConnectionInterface::class); + $this->configuration = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), self::PROJECT_ID, self::NAME - ); + ]); } public function testName() @@ -51,16 +51,16 @@ public function testName() public function testInfo() { - $this->adminConnection->getConfig(Argument::any())->shouldNotBeCalled(); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->connection->getConfig(Argument::any())->shouldNotBeCalled(); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $info = ['foo' => 'bar']; - $config = new ConfigurationStub( - $this->adminConnection->reveal(), + $config = \Google\Cloud\Dev\stub(Configuration::class, [ + $this->connection->reveal(), self::PROJECT_ID, self::NAME, $info - ); + ]); $this->assertEquals($info, $config->info()); } @@ -69,28 +69,28 @@ public function testInfoWithReload() { $info = ['foo' => 'bar']; - $this->adminConnection->getConfig([ + $this->connection->getConfig([ 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), 'projectId' => self::PROJECT_ID ])->shouldBeCalled()->willReturn($info); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $this->assertEquals($info, $this->configuration->info()); } public function testExists() { - $this->adminConnection->getConfig(Argument::any())->willReturn([]); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->connection->getConfig(Argument::any())->willReturn([]); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $this->assertTrue($this->configuration->exists()); } public function testExistsDoesntExist() { - $this->adminConnection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404)); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->connection->getConfig(Argument::any())->willThrow(new NotFoundException('', 404)); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $this->assertFalse($this->configuration->exists()); } @@ -99,12 +99,12 @@ public function testReload() { $info = ['foo' => 'bar']; - $this->adminConnection->getConfig([ + $this->connection->getConfig([ 'name' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, self::NAME), 'projectId' => self::PROJECT_ID ])->shouldBeCalledTimes(1)->willReturn($info); - $this->configuration->setAdminConnection($this->adminConnection->reveal()); + $this->configuration->___setProperty('connection', $this->connection->reveal()); $info = $this->configuration->reload(); @@ -113,11 +113,3 @@ public function testReload() $this->assertEquals($info, $info2); } } - -class ConfigurationStub extends Configuration -{ - public function setAdminConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/unit/SpannerAdmin/Connection/GrpcTest.php b/tests/unit/SpannerAdmin/Connection/GrpcTest.php new file mode 100644 index 000000000000..1eb2341e3b25 --- /dev/null +++ b/tests/unit/SpannerAdmin/Connection/GrpcTest.php @@ -0,0 +1,199 @@ +markTestSkipped('Must have the grpc extension installed to run this test.'); + } + + $this->requestWrapper = $this->prophesize(GrpcRequestWrapper::class); + $this->successMessage = 'success'; + } + + /** + * @dataProvider methodProvider + */ + public function testCallBasicMethods($method, $args, $expectedArgs) + { + $this->requestWrapper->send( + Argument::type('callable'), + $expectedArgs, + Argument::type('array') + )->willReturn($this->successMessage); + + $grpc = new Grpc(); + $grpc->setRequestWrapper($this->requestWrapper->reveal()); + + $this->assertEquals($this->successMessage, $grpc->$method($args)); + } + + public function methodProvider() + { + $configName = 'test-config'; + $instanceName = 'muh-instance'; + + $instanceArgs = [ + 'name' => $instanceName, + 'config' => 'foo', + 'displayName' => 'instanceName', + 'nodeCount' => 2, + 'state' => null + ]; + $instance = (new \google\spanner\admin\instance\v1\Instance())->deserialize($instanceArgs, new PhpArray()); + $fieldMask = (new \google\protobuf\FieldMask())->deserialize([ + 'paths' => [ + 'name', 'config', 'display_name', 'node_count' + ] + ], new PhpArray()); + + $databaseName = 'foo'; + $createStmt = 'CREATE DATABASE foo'; + $extraStmts = ['CREATE TABLE bar']; + + return [ + [ + 'listConfigs', + ['projectId' => self::PROJECT], + [self::PROJECT, []] + ], + [ + 'getConfig', + ['name' => $configName], + [$configName, []] + ], + [ + 'listInstances', + ['projectId' => self::PROJECT], + [self::PROJECT, []] + ], + [ + 'getInstance', + ['name' => $instanceName], + [$instanceName, []] + ], + [ + 'createInstance', + $instanceArgs + [ + 'projectId' => self::PROJECT, + 'instanceId' => $instanceName, + 'labels' => [] + ], + [self::PROJECT, $instanceName, $instance, []] + ], + [ + 'updateInstance', + $instanceArgs + [ + 'labels' => [] + ], + [$instance, $fieldMask, []] + ], + [ + 'deleteInstance', + ['name' => $instanceName], + [$instanceName, []] + ], + [ + 'getInstanceIamPolicy', + ['resource' => $instanceName], + [$instanceName, []] + ], + [ + 'setInstanceIamPolicy', + ['resource' => $instanceName, 'policy' => 'foo'], + [$instanceName, 'foo', []] + ], + [ + 'testInstanceIamPermissions', + ['resource' => $instanceName, 'permissions' => ['foo']], + [$instanceName, ['foo'], []] + ], + [ + 'listDatabases', + ['instance' => $instanceName], + [$instanceName, []] + ], + [ + 'createDatabase', + [ + 'instance' => $instanceName, + 'createStatement' => $createStmt, + 'extraStatements' => $extraStmts], + [$instanceName, $createStmt, $extraStmts, []] + ], + [ + 'updateDatabase', + [ + 'name' => $databaseName, + 'statements' => $extraStmts + ], + [$databaseName, $extraStmts, []] + ], + [ + 'dropDatabase', + ['name' => $databaseName], + [$databaseName, []] + ], + [ + 'getDatabaseDDL', + ['name' => $databaseName], + [$databaseName, []] + ], + [ + 'getDatabaseIamPolicy', + ['resource' => $databaseName], + [$databaseName, []] + ], + [ + 'setDatabaseIamPolicy', + [ + 'resource' => $databaseName, + 'policy' => 'foo' + ], + [$databaseName, 'foo', []] + ], + [ + 'testDatabaseIamPermissions', + [ + 'resource' => $databaseName, + 'permissions' => 'foo' + ], + [$databaseName, 'foo', []] + ], + ]; + } +} diff --git a/tests/Spanner/Connection/IamDatabaseTest.php b/tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php similarity index 75% rename from tests/Spanner/Connection/IamDatabaseTest.php rename to tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php index 870646654481..8138274b3204 100644 --- a/tests/Spanner/Connection/IamDatabaseTest.php +++ b/tests/unit/SpannerAdmin/Connection/IamDatabaseTest.php @@ -15,14 +15,14 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner\Connection; +namespace Google\Cloud\Tests\Unit\SpannerAdmin\Connection; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamDatabase; use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class IamDatabaseTest extends \PHPUnit_Framework_TestCase { @@ -32,9 +32,9 @@ class IamDatabaseTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->connection = $this->prophesize(AdminConnectionInterface::class); + $this->connection = $this->prophesize(ConnectionInterface::class); - $this->iam = new IamDatabaseStub($this->connection->reveal()); + $this->iam = \Google\Cloud\Dev\stub(IamDatabase::class, [$this->connection->reveal()]); } public function testGetPolicy() @@ -46,7 +46,7 @@ public function testGetPolicy() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->getPolicy($args); @@ -62,7 +62,7 @@ public function testSetPolicy() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->setPolicy($args); @@ -78,18 +78,10 @@ public function testTestPermissions() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->testPermissions($args); $this->assertEquals($res, $p); } } - -class IamDatabaseStub extends IamDatabase -{ - public function setConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/Spanner/Connection/IamInstanceTest.php b/tests/unit/SpannerAdmin/Connection/IamInstanceTest.php similarity index 75% rename from tests/Spanner/Connection/IamInstanceTest.php rename to tests/unit/SpannerAdmin/Connection/IamInstanceTest.php index 77cb7d12ed4c..06ebffceed4f 100644 --- a/tests/Spanner/Connection/IamInstanceTest.php +++ b/tests/unit/SpannerAdmin/Connection/IamInstanceTest.php @@ -15,14 +15,14 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner\Connection; +namespace Google\Cloud\Tests\Unit\SpannerAdmin\Connection; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Connection\IamInstance; use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class IamInstanceTest extends \PHPUnit_Framework_TestCase { @@ -32,9 +32,9 @@ class IamInstanceTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->connection = $this->prophesize(AdminConnectionInterface::class); + $this->connection = $this->prophesize(ConnectionInterface::class); - $this->iam = new IamInstanceStub($this->connection->reveal()); + $this->iam = \Google\Cloud\Dev\stub(IamInstance::class, [$this->connection->reveal()]); } public function testGetPolicy() @@ -46,7 +46,7 @@ public function testGetPolicy() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->getPolicy($args); @@ -62,7 +62,7 @@ public function testSetPolicy() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->setPolicy($args); @@ -78,18 +78,10 @@ public function testTestPermissions() ->shouldBeCalled() ->willReturn($res); - $this->iam->setConnection($this->connection->reveal()); + $this->iam->___setProperty('connection', $this->connection->reveal()); $p = $this->iam->testPermissions($args); $this->assertEquals($res, $p); } } - -class IamInstanceStub extends IamInstance -{ - public function setConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/Spanner/DatabaseTest.php b/tests/unit/SpannerAdmin/DatabaseTest.php similarity index 62% rename from tests/Spanner/DatabaseTest.php rename to tests/unit/SpannerAdmin/DatabaseTest.php index 5980cccffecb..712c31b393d9 100644 --- a/tests/Spanner/DatabaseTest.php +++ b/tests/unit/SpannerAdmin/DatabaseTest.php @@ -15,18 +15,19 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\SpannerAdmin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Session\SessionPoolInterface; use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class DatabaseTest extends \PHPUnit_Framework_TestCase { @@ -34,22 +35,23 @@ class DatabaseTest extends \PHPUnit_Framework_TestCase const INSTANCE_NAME = 'test-instance'; const NAME = 'test-database'; - private $adminConnection; + private $connection; private $instance; private $database; public function setUp() { - $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); + $this->connection = $this->prophesize(ConnectionInterface::class); $this->instance = $this->prophesize(Instance::class); $this->instance->name()->willReturn(self::INSTANCE_NAME); - $this->database = new DatabaseStub( - $this->adminConnection->reveal(), + $this->database = \Google\Cloud\Dev\stub(Database::class, [ + $this->connection->reveal(), $this->instance->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), self::PROJECT_ID, self::NAME - ); + ]); } public function testName() @@ -59,60 +61,73 @@ public function testName() public function testExists() { - $this->adminConnection->getDatabaseDDL(Argument::any()) + $this->connection->getDatabaseDDL(Argument::any()) ->shouldBeCalled() ->willReturn([]); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->assertTrue($this->database->exists()); } public function testExistsNotFound() { - $this->adminConnection->getDatabaseDDL(Argument::any()) + $this->connection->getDatabaseDDL(Argument::any()) ->shouldBeCalled() ->willThrow(new NotFoundException('', 404)); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->assertFalse($this->database->exists()); } - public function testUpdate() + public function testUpdateDdl() + { + $statement = 'foo'; + $this->connection->updateDatabase([ + 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), + 'statements' => [$statement] + ]); + + $this->database->___setProperty('connection', $this->connection->reveal()); + + $this->database->updateDdl($statement); + } + + public function testUpdateDdlBatch() { $statements = ['foo', 'bar']; - $this->adminConnection->updateDatabase([ + $this->connection->updateDatabase([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), 'statements' => $statements ]); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); - $this->database->update($statements); + $this->database->updateDdl($statements); } public function testUpdateWithSingleStatement() { $statement = 'foo'; - $this->adminConnection->updateDatabase([ + $this->connection->updateDatabase([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME), 'statements' => ['foo'], 'operationId' => null, ])->shouldBeCalled(); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); - $this->database->update($statement); + $this->database->updateDdl($statement); } public function testDrop() { - $this->adminConnection->dropDatabase([ + $this->connection->dropDatabase([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->shouldBeCalled(); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->database->drop(); } @@ -120,22 +135,22 @@ public function testDrop() public function testDdl() { $ddl = ['create table users', 'create table posts']; - $this->adminConnection->getDatabaseDDL([ + $this->connection->getDatabaseDDL([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->willReturn(['statements' => $ddl]); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->assertEquals($ddl, $this->database->ddl()); } public function testDdlNoResult() { - $this->adminConnection->getDatabaseDDL([ + $this->connection->getDatabaseDDL([ 'name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::INSTANCE_NAME, self::NAME) ])->willReturn([]); - $this->database->setAdminConnection($this->adminConnection->reveal()); + $this->database->___setProperty('connection', $this->connection->reveal()); $this->assertEquals([], $this->database->ddl()); } @@ -145,11 +160,3 @@ public function testIam() $this->assertInstanceOf(Iam::class, $this->database->iam()); } } - -class DatabaseStub extends Database -{ - public function setAdminConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/Spanner/InstanceTest.php b/tests/unit/SpannerAdmin/InstanceTest.php similarity index 64% rename from tests/Spanner/InstanceTest.php rename to tests/unit/SpannerAdmin/InstanceTest.php index 9f8d09a9c327..07e1eaf7a230 100644 --- a/tests/Spanner/InstanceTest.php +++ b/tests/unit/SpannerAdmin/InstanceTest.php @@ -15,33 +15,39 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\SpannerAdmin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Iam\Iam; use Google\Cloud\Spanner\Admin\Database\V1\DatabaseAdminClient; use Google\Cloud\Spanner\Admin\Instance\V1\InstanceAdminClient; use Google\Cloud\Spanner\Configuration; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; +use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Database; use Google\Cloud\Spanner\Instance; +use Google\Cloud\Spanner\Session\SessionPoolInterface; use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class InstanceTest extends \PHPUnit_Framework_TestCase { const PROJECT_ID = 'test-project'; const NAME = 'instance-name'; - private $adminConnection; + private $connection; private $instance; public function setUp() { - $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); - $this->instance = new InstanceStub($this->adminConnection->reveal(), self::PROJECT_ID, self::NAME); + $this->connection = $this->prophesize(ConnectionInterface::class); + $this->instance = \Google\Cloud\Dev\stub(Instance::class, [ + $this->connection->reveal(), + $this->prophesize(SessionPoolInterface::class)->reveal(), + self::PROJECT_ID, + self::NAME + ]); } public function testName() @@ -51,9 +57,9 @@ public function testName() public function testInfo() { - $this->adminConnection->getInstance()->shouldNotBeCalled(); + $this->connection->getInstance()->shouldNotBeCalled(); - $instance = new Instance($this->adminConnection->reveal(), self::PROJECT_ID, self::NAME, ['foo' => 'bar']); + $instance = new Instance($this->connection->reveal(), $this->prophesize(SessionPoolInterface::class)->reveal(), self::PROJECT_ID, self::NAME, false, ['foo' => 'bar']); $this->assertEquals('bar', $instance->info()['foo']); } @@ -61,11 +67,11 @@ public function testInfoWithReload() { $instance = $this->getDefaultInstance(); - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $info = $this->instance->info(); $this->assertEquals('Instance Name', $info['displayName']); @@ -75,20 +81,20 @@ public function testInfoWithReload() public function testExists() { - $this->adminConnection->getInstance(Argument::any())->shouldBeCalled()->willReturn([]); + $this->connection->getInstance(Argument::any())->shouldBeCalled()->willReturn([]); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->assertTrue($this->instance->exists()); } public function testExistsNotFound() { - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalled() ->willThrow(new NotFoundException('foo', 404)); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->assertFalse($this->instance->exists()); } @@ -97,11 +103,11 @@ public function testReload() { $instance = $this->getDefaultInstance(); - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $info = $this->instance->reload(); @@ -112,22 +118,22 @@ public function testState() { $instance = $this->getDefaultInstance(); - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->assertEquals(Instance::STATE_READY, $this->instance->state()); } public function testStateIsNull() { - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn([]); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->assertNull($this->instance->state()); } @@ -136,19 +142,18 @@ public function testUpdate() { $instance = $this->getDefaultInstance(); - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->adminConnection->updateInstance([ + $this->connection->updateInstance([ 'name' => $instance['name'], 'displayName' => $instance['displayName'], 'nodeCount' => $instance['nodeCount'], 'labels' => [], - 'config' => $instance['config'] ])->shouldBeCalled(); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->instance->update(); } @@ -158,19 +163,18 @@ public function testUpdateWithExistingLabels() $instance = $this->getDefaultInstance(); $instance['labels'] = ['foo' => 'bar']; - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->adminConnection->updateInstance([ + $this->connection->updateInstance([ 'name' => $instance['name'], 'displayName' => $instance['displayName'], 'nodeCount' => $instance['nodeCount'], 'labels' => $instance['labels'], - 'config' => $instance['config'] ])->shouldBeCalled(); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->instance->update(); } @@ -179,62 +183,37 @@ public function testUpdateWithChanges() { $instance = $this->getDefaultInstance(); - $config = $this->prophesize(Configuration::class); - $config->name()->willReturn('config-name'); - $changes = [ 'labels' => [ 'foo' => 'bar' ], 'nodeCount' => 900, 'displayName' => 'New Name', - 'config' => $config->reveal() ]; - $this->adminConnection->getInstance(Argument::any()) + $this->connection->getInstance(Argument::any()) ->shouldBeCalledTimes(1) ->willReturn($instance); - $this->adminConnection->updateInstance([ + $this->connection->updateInstance([ 'name' => $instance['name'], 'displayName' => $changes['displayName'], 'nodeCount' => $changes['nodeCount'], 'labels' => $changes['labels'], - 'config' => InstanceAdminClient::formatInstanceConfigName(self::PROJECT_ID, $changes['config']->name()) ])->shouldBeCalled(); - $this->instance->setAdminConnection($this->adminConnection->reveal()); - - $this->instance->update($changes); - } - - /** - * @expectedException InvalidArgumentException - */ - public function testUpdateInvalidConfig() - { - $instance = $this->getDefaultInstance(); - - $changes = [ - 'config' => 'foo' - ]; - - $this->adminConnection->getInstance(Argument::any()) - ->shouldBeCalledTimes(1) - ->willReturn($instance); - - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->instance->update($changes); } public function testDelete() { - $this->adminConnection->deleteInstance([ + $this->connection->deleteInstance([ 'name' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME) ])->shouldBeCalled(); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $this->instance->delete(); } @@ -247,7 +226,7 @@ public function testCreateDatabase() $extra = ['foo', 'bar']; - $this->adminConnection->createDatabase([ + $this->connection->createDatabase([ 'instance' => InstanceAdminClient::formatInstanceName(self::PROJECT_ID, self::NAME), 'createStatement' => 'CREATE DATABASE `test-database`', 'extraStatements' => $extra @@ -255,7 +234,7 @@ public function testCreateDatabase() ->shouldBeCalled() ->willReturn($dbInfo); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); $database = $this->instance->createDatabase('test-database', [ 'statements' => $extra @@ -279,11 +258,36 @@ public function testDatabases() ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] ]; - $this->adminConnection->listDatabases(Argument::any()) + $this->connection->listDatabases(Argument::any()) ->shouldBeCalled() ->willReturn(['databases' => $databases]); - $this->instance->setAdminConnection($this->adminConnection->reveal()); + $this->instance->___setProperty('connection', $this->connection->reveal()); + + $dbs = $this->instance->databases(); + + $this->assertInstanceOf(\Generator::class, $dbs); + + $dbs = iterator_to_array($dbs); + + $this->assertEquals(2, count($dbs)); + $this->assertEquals('database1', $dbs[0]->name()); + $this->assertEquals('database2', $dbs[1]->name()); + } + + public function testDatabasesPaged() + { + $databases = [ + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database1')], + ['name' => DatabaseAdminClient::formatDatabaseName(self::PROJECT_ID, self::NAME, 'database2')] + ]; + + $iteration = 0; + $this->connection->listDatabases(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn(['databases' => [$databases[0]], 'nextPageToken' => 'foo'], ['databases' => [$databases[1]]]); + + $this->instance->___setProperty('connection', $this->connection->reveal()); $dbs = $this->instance->databases(); @@ -308,11 +312,3 @@ private function getDefaultInstance() return json_decode(file_get_contents(__DIR__ .'/../fixtures/spanner/instance.json'), true); } } - -class InstanceStub extends Instance -{ - public function setAdminConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/Spanner/SpannerClientTest.php b/tests/unit/SpannerAdmin/SpannerClientTest.php similarity index 67% rename from tests/Spanner/SpannerClientTest.php rename to tests/unit/SpannerAdmin/SpannerClientTest.php index 0ec7198d7862..3bdd3a56616c 100644 --- a/tests/Spanner/SpannerClientTest.php +++ b/tests/unit/SpannerAdmin/SpannerClientTest.php @@ -15,18 +15,17 @@ * limitations under the License. */ -namespace Google\Cloud\Tests\Spanner; +namespace Google\Cloud\Tests\Unit\SpannerAdmin; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Spanner\Configuration; -use Google\Cloud\Spanner\Connection\AdminConnectionInterface; use Google\Cloud\Spanner\Connection\ConnectionInterface; use Google\Cloud\Spanner\Instance; use Google\Cloud\Spanner\SpannerClient; use Prophecy\Argument; /** - * @group spanner + * @group spanneradmin */ class SpannerClientTest extends \PHPUnit_Framework_TestCase { @@ -34,21 +33,17 @@ class SpannerClientTest extends \PHPUnit_Framework_TestCase private $connection; - private $adminConnection; - public function setUp() { $this->connection = $this->prophesize(ConnectionInterface::class); - $this->adminConnection = $this->prophesize(AdminConnectionInterface::class); - $this->client = new SpannerClientStub(['projectId' => 'test-project']); - $this->client->setConnection($this->connection->reveal()); - $this->client->setAdminConnection($this->adminConnection->reveal()); + $this->client = \Google\Cloud\Dev\stub(SpannerClient::class, [['projectId' => 'test-project']]); + $this->client->___setProperty('connection', $this->connection->reveal()); } public function testConfigurations() { - $this->adminConnection->listConfigs(Argument::any()) + $this->connection->listConfigs(Argument::any()) ->shouldBeCalled() ->willReturn([ 'instanceConfigs' => [ @@ -62,7 +57,44 @@ public function testConfigurations() ] ]); - $this->client->setAdminConnection($this->adminConnection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); + + $configs = $this->client->configurations(); + + $this->assertInstanceOf(\Generator::class, $configs); + + $configs = iterator_to_array($configs); + $this->assertEquals(2, count($configs)); + $this->assertInstanceOf(Configuration::class, $configs[0]); + $this->assertInstanceOf(Configuration::class, $configs[1]); + } + + public function testPagedConfigurations() + { + $firstCall = [ + 'instanceConfigs' => [ + [ + 'name' => 'projects/foo/instanceConfigs/bar', + 'displayName' => 'Bar' + ] + ], + 'nextPageToken' => 'fooBar' + ]; + + $secondCall = [ + 'instanceConfigs' => [ + [ + 'name' => 'projects/foo/instanceConfigs/bat', + 'displayName' => 'Bat' + ] + ] + ]; + + $this->connection->listConfigs(Argument::any()) + ->shouldBeCalledTimes(2) + ->willReturn($firstCall, $secondCall); + + $this->client->___setProperty('connection', $this->connection->reveal()); $configs = $this->client->configurations(); @@ -84,7 +116,7 @@ public function testConfiguration() public function testCreateInstance() { - $this->adminConnection->createInstance(Argument::that(function ($arg) { + $this->connection->createInstance(Argument::that(function ($arg) { if ($arg['name'] !== 'projects/test-project/instances/foo') return false; if ($arg['config'] !== 'projects/test-project/instanceConfigs/my-config') return false; @@ -93,7 +125,7 @@ public function testCreateInstance() ->shouldBeCalled() ->willReturn([]); - $this->client->setAdminConnection($this->adminConnection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $config = $this->prophesize(Configuration::class); $config->name()->willReturn('my-config'); @@ -119,7 +151,7 @@ public function testInstanceWithInstanceArray() public function testInstances() { - $this->adminConnection->listInstances(Argument::any()) + $this->connection->listInstances(Argument::any()) ->shouldBeCalled() ->willReturn([ 'instances' => [ @@ -128,7 +160,7 @@ public function testInstances() ] ]); - $this->client->setAdminConnection($this->adminConnection->reveal()); + $this->client->___setProperty('connection', $this->connection->reveal()); $instances = $this->client->instances(); $this->assertInstanceOf(\Generator::class, $instances); @@ -139,16 +171,3 @@ public function testInstances() $this->assertEquals('bar', $instances[1]->name()); } } - -class SpannerClientStub extends SpannerClient -{ - public function setConnection($conn) - { - $this->connection = $conn; - } - - public function setAdminConnection($conn) - { - $this->adminConnection = $conn; - } -} diff --git a/tests/fixtures/spanner/instance.json b/tests/unit/fixtures/spanner/instance.json similarity index 100% rename from tests/fixtures/spanner/instance.json rename to tests/unit/fixtures/spanner/instance.json From a39754c8c057cb33df1bc6885cf6445a8d9f8d3b Mon Sep 17 00:00:00 2001 From: michaelbausor Date: Wed, 15 Feb 2017 10:03:09 -0800 Subject: [PATCH 058/107] Add comment to getOperationsClient method (#334) --- src/Speech/V1beta1/SpeechClient.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Speech/V1beta1/SpeechClient.php b/src/Speech/V1beta1/SpeechClient.php index 8bd0f7d4f1dc..3634a7eb46a9 100644 --- a/src/Speech/V1beta1/SpeechClient.php +++ b/src/Speech/V1beta1/SpeechClient.php @@ -111,6 +111,11 @@ private static function getLongRunningDescriptors() ]; } + /** + * Return an OperationsClient object with the same endpoint as $this. + * + * @return \Google\GAX\LongRunning\OperationsClient + */ public function getOperationsClient() { return $this->operationsClient; From d051eb60747ec59c5c995bd1f2984fce61c343f9 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Wed, 15 Feb 2017 13:20:03 -0500 Subject: [PATCH 059/107] include null as valid data type (#332) --- src/Storage/Bucket.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Storage/Bucket.php b/src/Storage/Bucket.php index 4c0c43443662..89d0af05c7e6 100644 --- a/src/Storage/Bucket.php +++ b/src/Storage/Bucket.php @@ -182,7 +182,7 @@ public function exists() * @see https://cloud.google.com/storage/docs/json_api/v1/objects/insert Objects insert API documentation. * @see https://cloud.google.com/storage/docs/encryption#customer-supplied Customer-supplied encryption keys. * - * @param string|resource|StreamInterface $data The data to be uploaded. + * @param string|resource|StreamInterface|null $data The data to be uploaded. * @param array $options [optional] { * Configuration options. * @@ -265,7 +265,7 @@ public function upload($data, array $options = []) * uploads. * @see https://cloud.google.com/storage/docs/json_api/v1/objects/insert Objects insert API documentation. * - * @param string|resource|StreamInterface $data The data to be uploaded. + * @param string|resource|StreamInterface|null $data The data to be uploaded. * @param array $options [optional] { * Configuration options. * From 0f116a1aee39e0ff041b45064919c7256ee57432 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Fri, 17 Feb 2017 19:48:56 -0500 Subject: [PATCH 060/107] correct doc string to represent the fact the service detects the language of the provided content (#339) --- src/NaturalLanguage/NaturalLanguageClient.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/NaturalLanguage/NaturalLanguageClient.php b/src/NaturalLanguage/NaturalLanguageClient.php index 7d096e607d57..61c767e87084 100644 --- a/src/NaturalLanguage/NaturalLanguageClient.php +++ b/src/NaturalLanguage/NaturalLanguageClient.php @@ -128,7 +128,8 @@ public function __construct(array $config = []) * `PLAIN_TEXT` or `HTML`. **Defaults to** `"PLAIN_TEXT"`. * @type string $language The language of the document. Both ISO * (e.g., en, es) and BCP-47 (e.g., en-US, es-ES) language codes - * are accepted. Defaults to `"en"` (English). + * are accepted. If no value is provided, the language will be + * detected by the service. * @type string $encodingType The text encoding type used by the API to * calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`, * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. From 8c71e374ddee5634e7600af2ffaf1430b2294983 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Tue, 21 Feb 2017 10:51:25 -0500 Subject: [PATCH 061/107] Add support for Per-Object Storage Classes (#341) * update service definition * update documentation --- src/Storage/Bucket.php | 32 ++-- .../ServiceDefinition/storage-v1.json | 152 +++++++++++++----- src/Storage/StorageClient.php | 16 +- src/Storage/StorageObject.php | 33 ++-- 4 files changed, 157 insertions(+), 76 deletions(-) diff --git a/src/Storage/Bucket.php b/src/Storage/Bucket.php index 89d0af05c7e6..6f8260200ed3 100644 --- a/src/Storage/Bucket.php +++ b/src/Storage/Bucket.php @@ -198,10 +198,9 @@ public function exists() * you have increased reliability at the risk of higher overhead. * It is recommended to not use chunking. * @type string $predefinedAcl Predefined ACL to apply to the object. - * Acceptable values include, - * `"authenticatedRead"`, `"bucketOwnerFullControl"`, - * `"bucketOwnerRead"`, `"private"`, `"projectPrivate"`, and - * `"publicRead"`. + * Acceptable values include, `"authenticatedRead"`, + * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, + * `"projectPrivate"`, and `"publicRead"`. * @type array $metadata The available options for metadata are outlined * at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request-body). * @type string $encryptionKey A base64 encoded AES-256 customer-supplied @@ -279,9 +278,9 @@ public function upload($data, array $options = []) * you have increased reliability at the risk of higher overhead. * It is recommended to not use chunking. * @type string $predefinedAcl Predefined ACL to apply to the object. - * Acceptable values include `"authenticatedRead`", - * `"bucketOwnerFullControl`", `"bucketOwnerRead`", `"private`", - * `"projectPrivate`", and `"publicRead"`. + * Acceptable values include, `"authenticatedRead"`, + * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, + * `"projectPrivate"`, and `"publicRead"`. * @type array $metadata The available options for metadata are outlined * at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request-body). * @type string $encryptionKey A base64 encoded AES-256 customer-supplied @@ -384,7 +383,7 @@ public function object($name, array $options = []) * request. Defaults to `1000`. * @type string $prefix Filter results with this prefix. * @type string $projection Determines which properties to return. May - * be either 'full' or 'noAcl'. + * be either `"full"` or `"noAcl"`. * @type bool $versions If true, lists all versions of an object as * distinct results. The default is false. * @type string $fields Selector which will cause the response to only @@ -469,12 +468,14 @@ public function delete(array $options = []) * @type string $ifMetagenerationNotMatch Makes the return of the bucket * metadata conditional on whether the bucket's current * metageneration does not match the given value. - * @type string $predefinedAcl Apply a predefined set of access controls - * to this bucket. + * @type string $predefinedAcl Predefined ACL to apply to the bucket. + * Acceptable values include, `"authenticatedRead"`, + * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, + * `"projectPrivate"`, and `"publicRead"`. * @type string $predefinedDefaultObjectAcl Apply a predefined set of * default object access controls to this bucket. * @type string $projection Determines which properties to return. May - * be either 'full' or 'noAcl'. + * be either `"full"` or `"noAcl"`. * @type string $fields Selector which will cause the response to only * return the specified fields. * @type array $acl Access controls on the bucket. @@ -486,6 +487,11 @@ public function delete(array $options = []) * @type array $logging The bucket's logging configuration, which * defines the destination bucket and optional name prefix for the * current bucket's logs. + * @type string $storageClass The bucket's storage class. This defines + * how objects in the bucket are stored and determines the SLA and + * the cost of storage. Acceptable values include + * `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`, `"COLDLINE"`, + * `"STANDARD"` and `"DURABLE_REDUCED_AVAILABILITY"`. * @type array $versioning The bucket's versioning configuration. * @type array $website The bucket's website configuration. * } @@ -613,7 +619,7 @@ public function compose(array $sourceObjects, $name, array $options = []) * metadata conditional on whether the bucket's current * metageneration does not match the given value. * @type string $projection Determines which properties to return. May - * be either 'full' or 'noAcl'. + * be either `"full"` or `"noAcl"`. * } * @return array */ @@ -648,7 +654,7 @@ public function info(array $options = []) * metadata conditional on whether the bucket's current * metageneration does not match the given value. * @type string $projection Determines which properties to return. May - * be either 'full' or 'noAcl'. + * be either `"full"` or `"noAcl"`. * } * @return array */ diff --git a/src/Storage/Connection/ServiceDefinition/storage-v1.json b/src/Storage/Connection/ServiceDefinition/storage-v1.json index 17c4e581fa9b..a689ca629d0d 100644 --- a/src/Storage/Connection/ServiceDefinition/storage-v1.json +++ b/src/Storage/Connection/ServiceDefinition/storage-v1.json @@ -1,4 +1,29 @@ { + "kind": "discovery#restDescription", + "etag": "\"tbys6C40o18GZwyMen5GMkdK-3s/HgbrZgh9zgUkvtwaM_qfO-xyD4k\"", + "discoveryVersion": "v1", + "id": "storage:v1", + "name": "storage", + "version": "v1", + "revision": "20170208", + "title": "Cloud Storage JSON API", + "description": "Stores and retrieves potentially large, immutable data objects.", + "ownerDomain": "google.com", + "ownerName": "Google", + "icons": { + "x16": "https://www.google.com/images/icons/product/cloud_storage-16.png", + "x32": "https://www.google.com/images/icons/product/cloud_storage-32.png" + }, + "documentationLink": "https://developers.google.com/storage/docs/json_api/", + "labels": [ + "labs" + ], + "protocol": "rest", + "baseUrl": "https://www.googleapis.com/storage/v1/", + "basePath": "/storage/v1/", + "rootUrl": "https://www.googleapis.com/", + "servicePath": "storage/v1/", + "batchPath": "batch", "parameters": { "alt": { "type": "string", @@ -44,6 +69,27 @@ "location": "query" } }, + "auth": { + "oauth2": { + "scopes": { + "https://www.googleapis.com/auth/cloud-platform": { + "description": "View and manage your data across Google Cloud Platform services" + }, + "https://www.googleapis.com/auth/cloud-platform.read-only": { + "description": "View your data across Google Cloud Platform services" + }, + "https://www.googleapis.com/auth/devstorage.full_control": { + "description": "Manage your data and permissions in Google Cloud Storage" + }, + "https://www.googleapis.com/auth/devstorage.read_only": { + "description": "View your data in Google Cloud Storage" + }, + "https://www.googleapis.com/auth/devstorage.read_write": { + "description": "Manage your data in Google Cloud Storage" + } + } + } + }, "schemas": { "Bucket": { "id": "Bucket", @@ -110,7 +156,7 @@ }, "id": { "type": "string", - "description": "The ID of the bucket." + "description": "The ID of the bucket. For buckets, the id and name properities are the same." }, "kind": { "type": "string", @@ -131,9 +177,13 @@ "type": "object", "description": "The action to take.", "properties": { + "storageClass": { + "type": "string", + "description": "Target storage class. Required iff the type of the action is SetStorageClass." + }, "type": { "type": "string", - "description": "Type of the action. Currently, only Delete is supported." + "description": "Type of the action. Currently, only Delete and SetStorageClass are supported." } } }, @@ -155,6 +205,13 @@ "type": "boolean", "description": "Relevant only for versioned objects. If the value is true, this condition matches live objects; if the value is false, it matches archived objects." }, + "matchesStorageClass": { + "type": "array", + "description": "Objects having any of the storage classes specified by this condition will be matched. Values include MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, STANDARD, and DURABLE_REDUCED_AVAILABILITY.", + "items": { + "type": "string" + } + }, "numNewerVersions": { "type": "integer", "description": "Relevant only for versioned objects. If the value is N, this condition is satisfied when there are at least N versions (including the live version) newer than this version of the object.", @@ -224,7 +281,7 @@ }, "storageClass": { "type": "string", - "description": "The bucket's storage class. This defines how objects in the bucket are stored and determines the SLA and the cost of storage. Values include MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, STANDARD and DURABLE_REDUCED_AVAILABILITY. Defaults to STANDARD, which is equivalent to MULTI_REGIONAL or REGIONAL, depending on the bucket's location settings. For more information, see storage classes." + "description": "The bucket's default storage class, used whenever no storageClass is specified for a newly-created object. This defines how objects in the bucket are stored and determines the SLA and the cost of storage. Values include MULTI_REGIONAL, REGIONAL, STANDARD, NEARLINE, COLDLINE, and DURABLE_REDUCED_AVAILABILITY. If this value is not specified when the bucket is created, it will default to STANDARD. For more information, see storage classes." }, "timeCreated": { "type": "string", @@ -248,15 +305,15 @@ }, "website": { "type": "object", - "description": "The bucket's website configuration.", + "description": "The bucket's website configuration, controlling how the service behaves when accessing bucket contents as a web site. See the Static Website Examples for more information.", "properties": { "mainPageSuffix": { "type": "string", - "description": "Behaves as the bucket's directory index where missing objects are treated as potential directories." + "description": "If the requested object path is missing, the service will ensure the path has a trailing '/', append this suffix, and attempt to retrieve the resulting object. This allows the creation of index.html objects to represent directory pages." }, "notFoundPage": { "type": "string", - "description": "The custom object to return when a requested resource is not found." + "description": "If the requested object path is missing, and any mainPageSuffix object is missing, if applicable, the service will return the named object from this bucket as the content for a 404 Not Found result." } } } @@ -315,13 +372,13 @@ }, "team": { "type": "string", - "description": "The team. Can be owners, editors, or viewers." + "description": "The team." } } }, "role": { "type": "string", - "description": "The access permission for the entity. Can be READER, WRITER, or OWNER.", + "description": "The access permission for the entity.", "annotations": { "required": [ "storage.bucketAccessControls.insert" @@ -507,7 +564,7 @@ }, "cacheControl": { "type": "string", - "description": "Cache-Control directive for the object data." + "description": "Cache-Control directive for the object data. If omitted, and the object is accessible to all anonymous users, the default will be public, max-age=3600." }, "componentCount": { "type": "integer", @@ -528,7 +585,7 @@ }, "contentType": { "type": "string", - "description": "Content-Type of the object data." + "description": "Content-Type of the object data. If contentType is not specified, object downloads will be served as application/octet-stream." }, "crc32c": { "type": "string", @@ -559,7 +616,7 @@ }, "id": { "type": "string", - "description": "The ID of the object." + "description": "The ID of the object, including the bucket name, object name, and generation number." }, "kind": { "type": "string", @@ -589,7 +646,7 @@ }, "name": { "type": "string", - "description": "The name of this object. Required if not specified by URL parameter." + "description": "The name of the object. Required if not specified by URL parameter." }, "owner": { "type": "object", @@ -628,6 +685,11 @@ "description": "The deletion time of the object in RFC 3339 format. Will be returned if and only if this version of the object has been deleted.", "format": "date-time" }, + "timeStorageClassUpdated": { + "type": "string", + "description": "The time at which the object's storage class was last changed. When the object is initially created, it will be set to timeCreated.", + "format": "date-time" + }, "updated": { "type": "string", "description": "The modification time of the object metadata in RFC 3339 format.", @@ -654,7 +716,13 @@ }, "entity": { "type": "string", - "description": "The entity holding the permission, in one of the following forms: \n- user-userId \n- user-email \n- group-groupId \n- group-email \n- domain-domain \n- project-team-projectId \n- allUsers \n- allAuthenticatedUsers Examples: \n- The user liz@example.com would be user-liz@example.com. \n- The group example@googlegroups.com would be group-example@googlegroups.com. \n- To refer to all members of the Google Apps for Business domain example.com, the entity would be domain-example.com." + "description": "The entity holding the permission, in one of the following forms: \n- user-userId \n- user-email \n- group-groupId \n- group-email \n- domain-domain \n- project-team-projectId \n- allUsers \n- allAuthenticatedUsers Examples: \n- The user liz@example.com would be user-liz@example.com. \n- The group example@googlegroups.com would be group-example@googlegroups.com. \n- To refer to all members of the Google Apps for Business domain example.com, the entity would be domain-example.com.", + "annotations": { + "required": [ + "storage.defaultObjectAccessControls.insert", + "storage.objectAccessControls.insert" + ] + } }, "entityId": { "type": "string", @@ -666,7 +734,7 @@ }, "generation": { "type": "string", - "description": "The content generation of the object.", + "description": "The content generation of the object, if applied to an object.", "format": "int64" }, "id": { @@ -680,7 +748,7 @@ }, "object": { "type": "string", - "description": "The name of the object." + "description": "The name of the object, if applied to an object." }, "projectTeam": { "type": "object", @@ -692,13 +760,19 @@ }, "team": { "type": "string", - "description": "The team. Can be owners, editors, or viewers." + "description": "The team." } } }, "role": { "type": "string", - "description": "The access permission for the entity. Can be READER or OWNER." + "description": "The access permission for the entity.", + "annotations": { + "required": [ + "storage.defaultObjectAccessControls.insert", + "storage.objectAccessControls.insert" + ] + } }, "selfLink": { "type": "string", @@ -715,7 +789,7 @@ "type": "array", "description": "The list of items.", "items": { - "type": "any" + "$ref": "ObjectAccessControl" } }, "kind": { @@ -1042,7 +1116,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit acl and defaultObjectAcl properties." + "Omit owner, acl and defaultObjectAcl properties." ], "location": "query" } @@ -1122,7 +1196,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit acl and defaultObjectAcl properties." + "Omit owner, acl and defaultObjectAcl properties." ], "location": "query" } @@ -1180,7 +1254,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit acl and defaultObjectAcl properties." + "Omit owner, acl and defaultObjectAcl properties." ], "location": "query" } @@ -1203,7 +1277,7 @@ "id": "storage.buckets.patch", "path": "b/{bucket}", "httpMethod": "PATCH", - "description": "Updates a bucket. This method supports patch semantics.", + "description": "Updates a bucket. Changes to the bucket will be readable immediately after writing, but configuration changes may take time to propagate. This method supports patch semantics.", "parameters": { "bucket": { "type": "string", @@ -1272,7 +1346,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit acl and defaultObjectAcl properties." + "Omit owner, acl and defaultObjectAcl properties." ], "location": "query" } @@ -1288,15 +1362,14 @@ }, "scopes": [ "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" + "https://www.googleapis.com/auth/devstorage.full_control" ] }, "update": { "id": "storage.buckets.update", "path": "b/{bucket}", "httpMethod": "PUT", - "description": "Updates a bucket.", + "description": "Updates a bucket. Changes to the bucket will be readable immediately after writing, but configuration changes may take time to propagate.", "parameters": { "bucket": { "type": "string", @@ -1365,7 +1438,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit acl and defaultObjectAcl properties." + "Omit owner, acl and defaultObjectAcl properties." ], "location": "query" } @@ -1381,8 +1454,7 @@ }, "scopes": [ "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" + "https://www.googleapis.com/auth/devstorage.full_control" ] } } @@ -2030,7 +2102,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit the acl property." + "Omit the owner, acl property." ], "location": "query" }, @@ -2189,7 +2261,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit the acl property." + "Omit the owner, acl property." ], "location": "query" } @@ -2287,7 +2359,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit the acl property." + "Omit the owner, acl property." ], "location": "query" } @@ -2368,7 +2440,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit the acl property." + "Omit the owner, acl property." ], "location": "query" }, @@ -2471,7 +2543,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit the acl property." + "Omit the owner, acl property." ], "location": "query" } @@ -2488,8 +2560,7 @@ }, "scopes": [ "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" + "https://www.googleapis.com/auth/devstorage.full_control" ] }, "rewrite": { @@ -2594,7 +2665,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit the acl property." + "Omit the owner, acl property." ], "location": "query" }, @@ -2718,7 +2789,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit the acl property." + "Omit the owner, acl property." ], "location": "query" } @@ -2735,8 +2806,7 @@ }, "scopes": [ "https://www.googleapis.com/auth/cloud-platform", - "https://www.googleapis.com/auth/devstorage.full_control", - "https://www.googleapis.com/auth/devstorage.read_write" + "https://www.googleapis.com/auth/devstorage.full_control" ], "supportsMediaDownload": true, "useMediaDownloadService": true @@ -2784,7 +2854,7 @@ ], "enumDescriptions": [ "Include all properties.", - "Omit the acl property." + "Omit the owner, acl property." ], "location": "query" }, diff --git a/src/Storage/StorageClient.php b/src/Storage/StorageClient.php index 8b64cffc8186..baecd00ad40c 100644 --- a/src/Storage/StorageClient.php +++ b/src/Storage/StorageClient.php @@ -184,12 +184,16 @@ public function buckets(array $options = []) * @param array $options [optional] { * Configuration options. * - * @type string $predefinedAcl Apply a predefined set of access controls - * to this bucket. + * @type string $predefinedAcl Predefined ACL to apply to the bucket. + * Acceptable values include, `"authenticatedRead"`, + * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, + * `"projectPrivate"`, and `"publicRead"`. * @type string $predefinedDefaultObjectAcl Apply a predefined set of * default object access controls to this bucket. * @type string $projection Determines which properties to return. May - * be either 'full' or 'noAcl'. + * be either `"full"` or `"noAcl"`. **Defaults to** `"noAcl"`, + * unless the bucket resource specifies acl or defaultObjectAcl + * properties, when it defaults to `"full"`. * @type string $fields Selector which will cause the response to only * return the specified fields. * @type array $acl Access controls on the bucket. @@ -205,8 +209,10 @@ public function buckets(array $options = []) * current bucket's logs. * @type string $storageClass The bucket's storage class. This defines * how objects in the bucket are stored and determines the SLA and - * the cost of storage. Values include MULTI_REGIONAL, REGIONAL, - * NEARLINE, COLDLINE, STANDARD and DURABLE_REDUCED_AVAILABILITY. + * the cost of storage. Acceptable values include + * `"MULTI_REGIONAL"`, `"REGIONAL"`, `"NEARLINE"`, `"COLDLINE"`, + * `"STANDARD"` and `"DURABLE_REDUCED_AVAILABILITY"`. + * **Defaults to** `STANDARD`. * @type array $versioning The bucket's versioning configuration. * @type array $website The bucket's website configuration. * } diff --git a/src/Storage/StorageObject.php b/src/Storage/StorageObject.php index bac0f32ce074..bca14409a2d6 100644 --- a/src/Storage/StorageObject.php +++ b/src/Storage/StorageObject.php @@ -206,8 +206,10 @@ public function delete(array $options = []) * @type string $ifMetagenerationNotMatch Makes the operation * conditional on whether the object's current metageneration does * not match the given value. - * @type string $predefinedAcl Apply a predefined set of access controls - * to this object. + * @type string $predefinedAcl Predefined ACL to apply to the object. + * Acceptable values include, `"authenticatedRead"`, + * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, + * `"projectPrivate"`, and `"publicRead"`. * @type string $projection Determines which properties to return. May * be either 'full' or 'noAcl'. * @type string $fields Selector which will cause the response to only @@ -252,11 +254,10 @@ public function update(array $metadata, array $options = []) * * @type string $name The name of the destination object. **Defaults * to** the name of the source object. - * @type string $predefinedAcl Access controls to apply to the - * destination object. Acceptable values include - * `authenticatedRead`, `bucketOwnerFullControl`, - * `bucketOwnerRead`, `private`, `projectPrivate`, and - * `publicRead`. + * @type string $predefinedAcl Predefined ACL to apply to the object. + * Acceptable values include, `"authenticatedRead"`, + * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, + * `"projectPrivate"`, and `"publicRead"`. * @type string $encryptionKey A base64 encoded AES-256 customer-supplied * encryption key. It will be neccesary to provide this when a key * was used during the object's creation. @@ -359,11 +360,10 @@ public function copy($destination, array $options = []) * * @type string $name The name of the destination object. **Defaults * to** the name of the source object. - * @type string $predefinedAcl Access controls to apply to the - * destination object. Acceptable values include - * `authenticatedRead`, `bucketOwnerFullControl`, - * `bucketOwnerRead`, `private`, `projectPrivate`, and - * `publicRead`. + * @type string $predefinedAcl Predefined ACL to apply to the object. + * Acceptable values include, `"authenticatedRead"`, + * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, + * `"projectPrivate"`, and `"publicRead"`. * @type string $maxBytesRewrittenPerCall The maximum number of bytes * that will be rewritten per rewrite request. Most callers * shouldn't need to specify this parameter - it is primarily in @@ -460,11 +460,10 @@ public function rewrite($destination, array $options = []) * @param array $options [optional] { * Configuration options. * - * @type string $predefinedAcl Access controls to apply to the - * destination object. Acceptable values include - * `authenticatedRead`, `bucketOwnerFullControl`, - * `bucketOwnerRead`, `private`, `projectPrivate`, and - * `publicRead`. + * @type string $predefinedAcl Predefined ACL to apply to the object. + * Acceptable values include, `"authenticatedRead"`, + * `"bucketOwnerFullControl"`, `"bucketOwnerRead"`, `"private"`, + * `"projectPrivate"`, and `"publicRead"`. * @type string $encryptionKey A base64 encoded AES-256 customer-supplied * encryption key. It will be neccesary to provide this when a key * was used during the object's creation. From 1888d71be07aa576e217dfe3c57f4cae82ece730 Mon Sep 17 00:00:00 2001 From: Dan O'Meara Date: Tue, 21 Feb 2017 10:44:44 -0800 Subject: [PATCH 062/107] Minor adjustment to VisionClient introduction --- src/Vision/VisionClient.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Vision/VisionClient.php b/src/Vision/VisionClient.php index ae3dd47bba7a..d44aa345149c 100644 --- a/src/Vision/VisionClient.php +++ b/src/Vision/VisionClient.php @@ -24,7 +24,7 @@ use Psr\Cache\CacheItemPoolInterface; /** - * Google Cloud Vision client. Allows you to understand the content of an image, + * Google Cloud Vision client allows you to understand the content of an image, * classify images into categories, detect text, objects, faces and more. Find * more information at * [Google Cloud Vision docs](https://cloud.google.com/vision/docs/). From 4ac28cf7d9eefacb948db4296022b707b6d47830 Mon Sep 17 00:00:00 2001 From: Jeff Ching Date: Wed, 22 Feb 2017 10:14:55 -0800 Subject: [PATCH 063/107] Implement stream wrapper for gs:// (#323) * Create Storage\StreamWrapper implementation that handles reads. Add StreamWrapper registration instructions to README. Add ability to register/unregister the StreamWrapper. first stab at a StreamingUploader class Refactor uploader Add bucket->getStreamableUploader() Can pass storage options to the uploader and downloader streams via the stream context move register functions into the Storage namespace, autoloaded by composer add StreamingUploaderTest cannot lazy load because we need to know if the file open succeeded rename StreamingUploader to StreamableUploader to match convention Add test for StreamableUploader to test batching Add documentation for Bucket#getStreamableUploader with example. Adding tests for StreamWrapper with the most common use cases. Fix README with the correct usage * Fix php style guideline errors * Fix the remaining php coding standard errors. The method names need to be underscored instead of camel case because they are the callback methods needed to implement a stream wrapper. * Fix php 5.6 errors * Fix travis errors * Remove variadic from StreamableUploader. It is not compatible with php 5.5 * Throw a RuntimeException instead of dying if the StreamWrapper fails to register * Fix throwing of RuntimeException * Move Storage namespaced functions to static methods on StorageClient. * Use static functions on StorageClient to get/set the default client. Add a test for overriding the StorageClient in a stream wrapper via the context. * StreamWrapper will maintain its default StorageClient as a static variable. * Implement StorageClient#registerAsStreamWrapper(). Enables handling streams with that StorageClient instance as the default StorageClient. * Add documentation do StreamWrapper public functions * Catch ServiceExceptions instead of all GoogleExceptions * Pass the original exception to the re-raised GoogleException when a streaming upload fails * Fix stream wrapper checking array key for options * check array_key_exists on reading options * move codingStandardsIgnoreStart before the documentation for the stream handler callback functions. * Fix documentation on registering a StorageClient as a stream wrapper. * Fix building of docs for StreamWrapper class * Fix StreamWrapper loading of options from stream context in hhvm. In hhvm, you cannot get options from a null context. * Register the StorageClient with the protocol when registering a stream wrapper * Fix documentation for StreamWrapper getClient() and register() * Fix RuntimeException namespace * Remove getOption() which is no longer needed * Declare StreamableUploader constructor parameters. Declare $buffer visibility. * Can't use string type hints with defaults for php5 and hhvm * Stream the read when using the StreamWrapper * Fix code standard for test * Improve description of possible types for StreamableUploader#write() * Fix README registerStreamWrapper() * Removing type hints for non-object types * No need to lazy load or store options from context. Just read it once when it's needed (at stream_open time). * Namespace fixes to clean up code * Implement mkdir, rmdir, unlink, url_stat callbacks. Adding tests for these and directory operations, rename, stream_cast * Need to get the size of the stream since we are streaming our reads from http * We don't need to implement stream_cast for now. It doesn't seem to make sense for trying to stream_select on a gs:// path * Implement the directory callbacks and seek add seek tests Currently cannot seek on our stream wrapper Implement the directory callbacks * Implement rename * Fix CS for naming conventions * Fix tests for rewinding a directory * Handle the stream_open option flags. Add documentation about the allowed open modes. * Wrap download stream in a CachingStream if it must be seekable (getimagesize requires this) * Fix reference to debugging stream * Fix code style for isSeekable * Implement url_stat * Split buffering from the StreamableUploader into a WriteStream class (#2) * Split buffering from the StreamableUploader into a WriteStream class Implement ReadStream to wrap lazy downloaded stream to provide the size of the stream fix syntax error clean up stat array building Cleanup file mode constants Adding comments * Add ReadStream/WriteStream tests * Fixing stat tests * Add system tests for stream wrapper operations * Fix image test * Cannot use an array for constants * Fix unit tests to reflect mocks for refactored streams * Fixing system tests for url_stat for a directory without any files * Fix codestyle indentation issues. * Force our ReadStream to return the number of bytes requested. StreamInterface does not require this, however, the C internals do no expect that the stream returns less than the expected amount when filling its internal read buffer. This occurs specifically in getimagesize for jpeg images, when we try and read large amounts of exif data. * Documentation updates. Group private functions together. * Add custom phpcs ruleset. Ignore the camel case method name rule for StreamWrapper as the required callback method names are snake cased. * Updating docs for comments * ReadStream documentation updates * Handle Service exceptions when listing directories * rmdir now will attempt to delete the bucket if the path is '/' * Fix redefinition of method * Put the rename function back which was accidentally stomped * StreamWrapper can create the bucket and respect the specified mode. The mode int is extrapolated into one of publicRead, projectPrivate, or private. We will create a bucket if no path is given (i.e. gs://bucket-name-only/') or if the STREAM_MKDIR_RECURSIVE flag is provided. * Handle service exceptions when renaming files/directories * Can specify the chunkSize of the streaming upload via stream context. * Bucket->isWritable will now expect a 403 access denied. Any other ServiceException will be re-thrown. Also added some unit tests to account for this behavior. --- README.md | 15 + phpcs-ruleset.xml | 3 + src/Storage/Bucket.php | 99 ++- src/Storage/Connection/Rest.php | 27 +- src/Storage/ReadStream.php | 93 +++ src/Storage/StorageClient.php | 21 + src/Storage/StorageObject.php | 9 +- src/Storage/StreamWrapper.php | 674 ++++++++++++++++++ src/Storage/WriteStream.php | 110 +++ src/Upload/ResumableUploader.php | 2 +- src/Upload/StreamableUploader.php | 85 +++ tests/system/Storage/StorageTestCase.php | 5 +- .../Storage/StreamWrapper/DirectoryTest.php | 84 +++ .../Storage/StreamWrapper/ImageTest.php | 78 ++ .../system/Storage/StreamWrapper/ReadTest.php | 63 ++ .../Storage/StreamWrapper/RenameTest.php | 54 ++ .../StreamWrapper/StreamWrapperTestCase.php | 46 ++ .../Storage/StreamWrapper/UrlStatTest.php | 107 +++ .../Storage/StreamWrapper/WriteTest.php | 73 ++ tests/unit/Storage/BucketTest.php | 29 + tests/unit/Storage/ReadStreamTest.php | 71 ++ tests/unit/Storage/StorageClientTest.php | 9 + tests/unit/Storage/StreamWrapperTest.php | 426 +++++++++++ tests/unit/Storage/WriteStreamTest.php | 56 ++ tests/unit/Upload/StreamableUploaderTest.php | 147 ++++ 25 files changed, 2364 insertions(+), 22 deletions(-) create mode 100644 src/Storage/ReadStream.php create mode 100644 src/Storage/StreamWrapper.php create mode 100644 src/Storage/WriteStream.php create mode 100644 src/Upload/StreamableUploader.php create mode 100644 tests/system/Storage/StreamWrapper/DirectoryTest.php create mode 100644 tests/system/Storage/StreamWrapper/ImageTest.php create mode 100644 tests/system/Storage/StreamWrapper/ReadTest.php create mode 100644 tests/system/Storage/StreamWrapper/RenameTest.php create mode 100644 tests/system/Storage/StreamWrapper/StreamWrapperTestCase.php create mode 100644 tests/system/Storage/StreamWrapper/UrlStatTest.php create mode 100644 tests/system/Storage/StreamWrapper/WriteTest.php create mode 100644 tests/unit/Storage/ReadStreamTest.php create mode 100644 tests/unit/Storage/StreamWrapperTest.php create mode 100644 tests/unit/Storage/WriteStreamTest.php create mode 100644 tests/unit/Upload/StreamableUploaderTest.php diff --git a/README.md b/README.md index 0c6a91082dfb..862252da3174 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,21 @@ $object = $bucket->object('file_backup.txt'); $object->downloadToFile('/data/file_backup.txt'); ``` +#### Stream Wrapper + +```php +require 'vendor/autoload.php'; + +use Google\Cloud\Storage\StorageClient; + +$storage = new StorageClient([ + 'projectId' => 'my_project' +]); +$storage->registerStreamWrapper(); + +$contents = file_get_contents('gs://my_bucket/file_backup.txt'); +``` + ## Google Cloud Translation (Alpha) - [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/translate/translateclient) diff --git a/phpcs-ruleset.xml b/phpcs-ruleset.xml index ed53b7bbb602..d08453666091 100644 --- a/phpcs-ruleset.xml +++ b/phpcs-ruleset.xml @@ -4,5 +4,8 @@ src/*/V[0-9]+ + + src/Storage/StreamWrapper.php + src diff --git a/src/Storage/Bucket.php b/src/Storage/Bucket.php index 6f8260200ed3..ceb3d7c30c4a 100644 --- a/src/Storage/Bucket.php +++ b/src/Storage/Bucket.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Storage; use Google\Cloud\Exception\NotFoundException; +use Google\Cloud\Exception\ServiceException; use Google\Cloud\Storage\Connection\ConnectionInterface; use Google\Cloud\Upload\ResumableUploader; use GuzzleHttp\Psr7; @@ -273,6 +274,67 @@ public function upload($data, array $options = []) * applied using md5 hashing functionality. If true and the * calculated hash does not match that of the upstream server the * upload will be rejected. + * @type string $predefinedAcl Predefined ACL to apply to the object. + * Acceptable values include `"authenticatedRead`", + * `"bucketOwnerFullControl`", `"bucketOwnerRead`", `"private`", + * `"projectPrivate`", and `"publicRead"`. + * @type array $metadata The available options for metadata are outlined + * at the [JSON API docs](https://cloud.google.com/storage/docs/json_api/v1/objects/insert#request-body). + * @type string $encryptionKey A base64 encoded AES-256 customer-supplied + * encryption key. + * @type string $encryptionKeySHA256 Base64 encoded SHA256 hash of the + * customer-supplied encryption key. This value will be calculated + * from the `encryptionKey` on your behalf if not provided, but + * for best performance it is recommended to pass in a cached + * version of the already calculated SHA. + * } + * @return ResumableUploader + * @throws \InvalidArgumentException + */ + public function getResumableUploader($data, array $options = []) + { + if (is_string($data) && !isset($options['name'])) { + throw new \InvalidArgumentException('A name is required when data is of type string.'); + } + + return $this->connection->insertObject( + $this->formatEncryptionHeaders($options) + [ + 'bucket' => $this->identity['bucket'], + 'data' => $data, + 'resumable' => true + ] + ); + } + + /** + * Get a streamable uploader which can provide greater control over the + * upload process. This is useful for generating large files and uploading + * the contents in chunks. + * + * Example: + * ``` + * $uploader = $bucket->getStreamableUploader( + * 'initial contents', + * ['name' => 'data.txt'] + * ); + * + * // finish uploading the item + * $uploader->upload(); + * ``` + * + * @see https://cloud.google.com/storage/docs/json_api/v1/how-tos/upload#resumable Learn more about resumable + * uploads. + * @see https://cloud.google.com/storage/docs/json_api/v1/objects/insert Objects insert API documentation. + * + * @param string|resource|StreamInterface $data The data to be uploaded. + * @param array $options [optional] { + * Configuration options. + * + * @type string $name The name of the destination. + * @type bool $validate Indicates whether or not validation will be + * applied using md5 hashing functionality. If true and the + * calculated hash does not match that of the upstream server the + * upload will be rejected. * @type int $chunkSize If provided the upload will be done in chunks. * The size must be in multiples of 262144 bytes. With chunking * you have increased reliability at the risk of higher overhead. @@ -291,10 +353,10 @@ public function upload($data, array $options = []) * for best performance it is recommended to pass in a cached * version of the already calculated SHA. * } - * @return ResumableUploader + * @return StreamableUploader * @throws \InvalidArgumentException */ - public function getResumableUploader($data, array $options = []) + public function getStreamableUploader($data, array $options = []) { if (is_string($data) && !isset($options['name'])) { throw new \InvalidArgumentException('A name is required when data is of type string.'); @@ -304,7 +366,8 @@ public function getResumableUploader($data, array $options = []) $this->formatEncryptionHeaders($options) + [ 'bucket' => $this->identity['bucket'], 'data' => $data, - 'resumable' => true + 'streamable' => true, + 'validate' => false ] ); } @@ -677,4 +740,34 @@ public function name() { return $this->identity['bucket']; } + + /** + * Returns whether the bucket with the given file prefix is writable. + * Tries to create a temporary file as a resumable upload which will + * not be completed (and cleaned up by GCS). + * + * @param string $file Optional file to try to write. + * @return boolean + * @throws ServiceException + */ + public function isWritable($file = null) + { + $file = $file ?: '__tempfile'; + $uploader = $this->getResumableUploader( + Psr7\stream_for(''), + ['name' => $file] + ); + try { + $uploader->getResumeUri(); + } catch (ServiceException $e) { + // We expect a 403 access denied error if the bucket is not writable + if ($e->getCode() == 403) { + return false; + } + // If not a 403, re-raise the unexpected error + throw $e; + } + + return true; + } } diff --git a/src/Storage/Connection/Rest.php b/src/Storage/Connection/Rest.php index 6e4ea62cea14..7d71d2e3262c 100644 --- a/src/Storage/Connection/Rest.php +++ b/src/Storage/Connection/Rest.php @@ -24,6 +24,7 @@ use Google\Cloud\Upload\AbstractUploader; use Google\Cloud\Upload\MultipartUploader; use Google\Cloud\Upload\ResumableUploader; +use Google\Cloud\Upload\StreamableUploader; use Google\Cloud\UriTrait; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Request; @@ -230,10 +231,16 @@ public function downloadObject(array $args = []) public function insertObject(array $args = []) { $args = $this->resolveUploadOptions($args); - $isResumable = $args['resumable']; - $uploadType = $isResumable - ? AbstractUploader::UPLOAD_TYPE_RESUMABLE - : AbstractUploader::UPLOAD_TYPE_MULTIPART; + + $uploadType = AbstractUploader::UPLOAD_TYPE_RESUMABLE; + if ($args['streamable']) { + $uploaderClass = StreamableUploader::class; + } elseif ($args['resumable']) { + $uploaderClass = ResumableUploader::class; + } else { + $uploaderClass = MultipartUploader::class; + $uploadType = AbstractUploader::UPLOAD_TYPE_MULTIPART; + } $uriParams = [ 'bucket' => $args['bucket'], @@ -243,16 +250,7 @@ public function insertObject(array $args = []) ] ]; - if ($isResumable) { - return new ResumableUploader( - $this->requestWrapper, - $args['data'], - $this->expandUri(self::UPLOAD_URI, $uriParams), - $args['uploaderOptions'] - ); - } - - return new MultipartUploader( + return new $uploaderClass( $this->requestWrapper, $args['data'], $this->expandUri(self::UPLOAD_URI, $uriParams), @@ -270,6 +268,7 @@ private function resolveUploadOptions(array $args) 'name' => null, 'validate' => true, 'resumable' => null, + 'streamable' => null, 'predefinedAcl' => null, 'metadata' => [] ]; diff --git a/src/Storage/ReadStream.php b/src/Storage/ReadStream.php new file mode 100644 index 000000000000..33e55094ca4c --- /dev/null +++ b/src/Storage/ReadStream.php @@ -0,0 +1,93 @@ +stream = $stream; + } + + /** + * Return the full size of the buffer. If the underlying stream does + * not report it's size, try to fetch the size from the Content-Length + * response header. + * + * @return int The size of the stream. + */ + public function getSize() + { + return $this->stream->getSize() ?: $this->getSizeFromMetadata(); + } + + /** + * Attempt to fetch the size from the Content-Length response header. + * If we cannot, return 0. + * + * @return int The Size of the stream + */ + private function getSizeFromMetadata() + { + foreach ($this->stream->getMetadata('wrapper_data') as $value) { + if (substr($value, 0, 15) == "Content-Length:") { + return (int) substr($value, 16); + } + } + return 0; + } + + /** + * Read bytes from the underlying buffer, retrying until we have read + * enough bytes or we cannot read any more. We do this because the + * internal C code for filling a buffer does not account for when + * we try to read large chunks from a user-land stream that does not + * return enough bytes. + * + * @param int $length The number of bytes to read. + * @return string Read bytes from the underlying stream. + */ + public function read($length) + { + $data = ''; + do { + $moreData = $this->stream->read($length); + $data .= $moreData; + $readLength = strlen($moreData); + $length -= $readLength; + } while ($length > 0 && $readLength > 0); + + return $data; + } +} diff --git a/src/Storage/StorageClient.php b/src/Storage/StorageClient.php index baecd00ad40c..0caa41313d39 100644 --- a/src/Storage/StorageClient.php +++ b/src/Storage/StorageClient.php @@ -223,4 +223,25 @@ public function createBucket($name, array $options = []) $response = $this->connection->insertBucket($options + ['name' => $name, 'project' => $this->projectId]); return new Bucket($this->connection, $name, $response); } + + /** + * Registers this StorageClient as the handler for stream reading/writing. + * + * @param string $protocol The name of the protocol to use. **Defaults to** `gs`. + * @throws \RuntimeException + */ + public function registerStreamWrapper($protocol = null) + { + return StreamWrapper::register($this, $protocol); + } + + /** + * Unregisters the SteamWrapper + * + * @param string $protocol The name of the protocol to unregister. **Defaults to** `gs`. + */ + public function unregisterStreamWrapper($protocol = null) + { + StreamWrapper::unregister($protocol); + } } diff --git a/src/Storage/StorageObject.php b/src/Storage/StorageObject.php index bca14409a2d6..eaada957add2 100644 --- a/src/Storage/StorageObject.php +++ b/src/Storage/StorageObject.php @@ -496,12 +496,19 @@ public function rewrite($destination, array $options = []) * @type string $ifSourceMetagenerationNotMatch Makes the operation * conditional on whether the source object's current * metageneration does not match the given value. + * @type string $destinationBucket Will move to this bucket if set. If + * not set, will default to the same bucket. * } * @return StorageObject The renamed object. */ public function rename($name, array $options = []) { - $copiedObject = $this->copy($this->identity['bucket'], [ + $destinationBucket = isset($options['destinationBucket']) + ? $options['destinationBucket'] + : $this->identity['bucket']; + unset($options['destinationBucket']); + + $copiedObject = $this->copy($destinationBucket, [ 'name' => $name ] + $options); diff --git a/src/Storage/StreamWrapper.php b/src/Storage/StreamWrapper.php new file mode 100644 index 000000000000..f5e9e83c8a10 --- /dev/null +++ b/src/Storage/StreamWrapper.php @@ -0,0 +1,674 @@ +stream_close(); + } + + /** + * Register a StreamWrapper for reading and writing to Google Storage + * + * @param StorageClient $client The StorageClient configuration to use. + * @param string $protocol The name of the protocol to use. **Defaults to** + * `gs`. + * @throws \RuntimeException + */ + public static function register(StorageClient $client, $protocol = null) + { + $protocol = $protocol ?: self::DEFAULT_PROTOCOL; + if (!in_array($protocol, stream_get_wrappers())) { + if (!stream_wrapper_register($protocol, StreamWrapper::class, STREAM_IS_URL)) { + throw new \RuntimeException("Failed to register '$protocol://' protocol"); + } + self::$clients[$protocol] = $client; + return true; + } + return false; + } + + /** + * Unregisters the SteamWrapper + * + * @param string $protocol The name of the protocol to unregister. **Defaults + * to** `gs`. + */ + public static function unregister($protocol = null) + { + $protocol = $protocol ?: self::DEFAULT_PROTOCOL; + stream_wrapper_unregister($protocol); + unset(self::$clients[$protocol]); + } + + /** + * Get the default client to use for streams. + * + * @param string $protocol The name of the protocol to get the client for. + * **Defaults to** `gs`. + * @return StorageClient + */ + public static function getClient($protocol = null) + { + $protocol = $protocol ?: self::DEFAULT_PROTOCOL; + return self::$clients[$protocol]; + } + + /** + * Callback handler for when a stream is opened. For reads, we need to + * download the file to see if it can be opened. + * + * @param string $path The path of the resource to open + * @param string $mode The fopen mode. Currently only supports ('r', 'rb', 'rt', 'w', 'wb', 'wt') + * @param int $flags Bitwise options STREAM_USE_PATH|STREAM_REPORT_ERRORS|STREAM_MUST_SEEK + * @param string $openedPath Will be set to the path on success if STREAM_USE_PATH option is set + * @return bool + */ + public function stream_open($path, $mode, $flags, &$openedPath) + { + $client = $this->openPath($path); + + // strip off 'b' or 't' from the mode + $mode = rtrim($mode, 'bt'); + + $options = []; + if ($this->context) { + $contextOptions = stream_context_get_options($this->context); + if (array_key_exists($this->protocol, $contextOptions)) { + $options = $contextOptions[$this->protocol] ?: []; + } + } + + if ($mode == 'w') { + $this->stream = new WriteStream(null, $options); + $this->stream->setUploader( + $this->bucket->getStreamableUploader( + $this->stream, + $options + ['name' => $this->file] + ) + ); + } elseif ($mode == 'r') { + try { + // Lazy read from the source + $options['httpOptions']['stream'] = true; + $this->stream = new ReadStream( + $this->bucket->object($this->file)->downloadAsStream($options) + ); + + // Wrap the response in a caching stream to make it seekable + if (!$this->stream->isSeekable() && ($flags & STREAM_MUST_SEEK)) { + $this->stream = new CachingStream($this->stream); + } + } catch (ServiceException $ex) { + return $this->returnError($ex->getMessage(), $flags); + } + } else { + return $this->returnError('Unknown stream_open mode.', $flags); + } + + if ($flags & STREAM_USE_PATH) { + $openedPath = $path; + } + return true; + } + + /** + * Callback handler for when we try to read a certain number of bytes. + * + * @param int $count The number of bytes to read. + * + * @return string + */ + public function stream_read($count) + { + return $this->stream->read($count); + } + + /** + * Callback handler for when we try to write data to the stream. + * + * @param string $data The data to write + * + * @return int The number of bytes written. + */ + public function stream_write($data) + { + return $this->stream->write($data); + } + + /** + * Callback handler for getting data about the stream. + * + * @return array + */ + public function stream_stat() + { + $mode = $this->stream->isWritable() + ? self::FILE_WRITABLE_MODE + : self::FILE_READABLE_MODE; + return $this->makeStatArray([ + 'mode' => $mode, + 'size' => $this->stream->getSize() + ]); + } + + /** + * Callback handler for checking to see if the stream is at the end of file. + * + * @return bool + */ + public function stream_eof() + { + return $this->stream->eof(); + } + + /** + * Callback handler for trying to close the stream. + */ + public function stream_close() + { + if (isset($this->stream)) { + $this->stream->close(); + } + } + + /** + * Callback handler for trying to seek to a certain location in the stream. + * + * @param int $offset The stream offset to seek to + * @param int $whence Flag for what the offset is relative to. See: + * http://php.net/manual/en/streamwrapper.stream-seek.php + * @return bool + */ + public function stream_seek($offset, $whence = SEEK_SET) + { + if ($this->stream->isSeekable()) { + $this->stream->seek($offset, $whence); + return true; + } + return false; + } + + /** + * Callhack handler for inspecting our current position in the stream + * + * @return int + */ + public function stream_tell() + { + return $this->stream->tell(); + } + + /** + * Callback handler for trying to close an opened directory. + * + * @return bool + */ + public function dir_closedir() + { + return false; + } + + /** + * Callback handler for trying to open a directory. + * + * @param string $path The url directory to open + * @param int $options Whether or not to enforce safe_mode + * @return bool + */ + public function dir_opendir($path, $options) + { + $this->openPath($path); + return $this->dir_rewinddir(); + } + + /** + * Callback handler for reading an entry from a directory handle. + * + * @return string|bool + */ + public function dir_readdir() + { + $object = $this->directoryGenerator->current(); + if ($object) { + $this->directoryGenerator->next(); + return $object->name(); + } else { + return false; + } + } + + /** + * Callback handler for rewind the directory handle. + * + * @return bool + */ + public function dir_rewinddir() + { + try { + $this->directoryGenerator = $this->bucket->objects([ + 'prefix' => $this->file, + 'fields' => 'items/name,nextPageToken' + ]); + } catch (ServiceException $e) { + return false; + } + return true; + } + + /** + * Callback handler for trying to create a directory. If no file path is specified, + * or STREAM_MKDIR_RECURSIVE option is set, then create the bucket if it does not exist. + * + * @param string $path The url directory to create + * @param int $mode The permissions on the directory + * @param int $options Bitwise mask of options. STREAM_MKDIR_RECURSIVE + * @return bool + */ + public function mkdir($path, $mode, $options) + { + $path = $this->makeDirectory($path); + $client = $this->openPath($path); + $predefinedAcl = $this->determineAclFromMode($mode); + + try { + if ($options & STREAM_MKDIR_RECURSIVE || $this->file == '') { + if (!$this->bucket->exists()) { + $client->createBucket($this->bucket->name(), [ + 'predefinedAcl' => $predefinedAcl, + 'predefinedDefaultObjectAcl' => $predefinedAcl + ]); + } + } + + // If the file name is empty, we were trying to create a bucket. In this case, + // don't create the placeholder file. + if ($this->file != '') { + // Fake a directory by creating an empty placeholder file whose name ends in '/' + $this->bucket->upload('', [ + 'name' => $this->file, + 'predefinedAcl' => $predefinedAcl + ]); + } + } catch (ServiceException $e) { + return false; + } + return true; + } + + /** + * Callback handler for trying to move a file or directory. + * + * @param string $from The URL to the current file + * @param string $to The URL of the new file location + * @return bool + */ + public function rename($from, $to) + { + $url = parse_url($to); + $destinationBucket = $url['host']; + $destinationPath = substr($url['path'], 1); + + $this->dir_opendir($from, []); + foreach ($this->directoryGenerator as $file) { + $name = $file->name(); + $newPath = str_replace($this->file, $destinationPath, $name); + + $obj = $this->bucket->object($name); + try { + $obj->rename($newPath, ['destinationBucket' => $destinationBucket]); + } catch (ServiceException $e) { + // If any rename calls fail, abort and return false + return false; + } + } + return true; + } + + /** + * Callback handler for trying to remove a directory or a bucket. If the path is empty + * or '/', the bucket will be deleted. + * + * Note that the STREAM_MKDIR_RECURSIVE flag is ignored because the option cannot + * be set via the `rmdir()` function. + * + * @param string $path The URL directory to remove. If the path is empty or is '/', + * This will attempt to destroy the bucket. + * @param int $options Bitwise mask of options. + * @return bool + */ + public function rmdir($path, $options) + { + $path = $this->makeDirectory($path); + $this->openPath($path); + + try { + if ($this->file == '') { + $this->bucket->delete(); + return true; + } else { + return $this->unlink($path); + } + } catch (ServiceException $e) { + return false; + } + } + + /** + * Callback handler for retrieving the underlaying resource + * + * @param int $castAs STREAM_CAST_FOR_SELECT|STREAM_CAST_AS_STREAM + * @return resource|bool + */ + public function stream_cast($castAs) + { + return false; + } + + /** + * Callback handler for deleting a file + * + * @param string $path The URL of the file to delete + * @return bool + */ + public function unlink($path) + { + $client = $this->openPath($path); + $object = $this->bucket->object($this->file); + + try { + $object->delete(); + return true; + } catch (ServiceException $e) { + return false; + } + } + + /** + * Callback handler for retrieving information about a file + * + * @param string $path The URI to the file + * @param int $flags Bitwise mask of options + * @return array|bool + */ + public function url_stat($path, $flags) + { + $client = $this->openPath($path); + + // if directory + if ($this->isDirectory($this->file)) { + return $this->urlStatDirectory(); + } else { + return $this->urlStatFile(); + } + } + + /** + * Parse the URL and set protocol, filename and bucket. + * + * @param string $path URL to open + * @return StorageClient + */ + private function openPath($path) + { + $url = parse_url($path); + $this->protocol = $url['scheme']; + $this->file = ltrim($url['path'], '/'); + $client = self::getClient($this->protocol); + $this->bucket = $client->bucket($url['host']); + return $client; + } + + /** + * Given a path, ensure that we return a path that looks like a directory + * + * @param string $path + * @return string + */ + private function makeDirectory($path) + { + if (substr($path, -1) == '/') { + return $path; + } else { + return $path . '/'; + } + } + + /** + * Calculate the `url_stat` response for a directory + * + * @return array|bool + */ + private function urlStatDirectory() + { + $stats = []; + // 1. try to look up the directory as a file + try { + $this->object = $this->bucket->object($this->file); + $info = $this->object->info(); + + // equivalent to 40777 and 40444 in octal + $stats['mode'] = $this->bucket->isWritable() + ? self::DIRECTORY_WRITABLE_MODE + : self::DIRECTORY_READABLE_MODE; + $this->statsFromFileInfo($info, $stats); + + return $this->makeStatArray($stats); + } catch (NotFoundException $e) { + } catch (ServiceException $e) { + return false; + } + + // 2. try list files in that directory + try { + $objects = $this->bucket->objects([ + 'prefix' => $this->file, + ]); + + if (!$objects->current()) { + // can't list objects or doesn't exist + return false; + } + } catch (ServiceException $e) { + return false; + } + + // equivalent to 40777 and 40444 in octal + $mode = $this->bucket->isWritable() + ? self::DIRECTORY_WRITABLE_MODE + : self::DIRECTORY_READABLE_MODE; + return $this->makeStatArray([ + 'mode' => $mode + ]); + } + + /** + * Calculate the `url_stat` response for a file + * + * @return array|bool + */ + private function urlStatFile() + { + try { + $this->object = $this->bucket->object($this->file); + $info = $this->object->info(); + } catch (ServiceException $e) { + // couldn't stat file + return false; + } + + // equivalent to 100666 and 100444 in octal + $stats = array( + 'mode' => $this->bucket->isWritable() + ? self::FILE_WRITABLE_MODE + : self::FILE_READABLE_MODE + ); + $this->statsFromFileInfo($info, $stats); + return $this->makeStatArray($stats); + } + + /** + * Given a `StorageObject` info array, extract the available fields into the + * provided `$stats` array. + * + * @param array $info Array provided from a `StorageObject`. + * @param array $stats Array to put the calculated stats into. + */ + private function statsFromFileInfo(array &$info, array &$stats) + { + $stats['size'] = (int) $info['size']; + $stats['mtime'] = strtotime($info['updated']); + $stats['ctime'] = strtotime($info['timeCreated']); + } + + /** + * Return whether we think the provided path is a directory or not + * + * @param string $path + * @return bool + */ + private function isDirectory($path) + { + return substr($path, -1) == '/'; + } + + /** + * Returns the associative array that a `stat()` response expects using the + * provided stats. Defaults the remaining fields to 0. + * + * @param array $stats Sparse stats entries to set. + * @return array + */ + private function makeStatArray($stats) + { + return array_merge( + array_fill_keys([ + 'dev', + 'ino', + 'mode', + 'nlink', + 'uid', + 'gid', + 'rdev', + 'size', + 'atime', + 'mtime', + 'ctime', + 'blksize', + 'blocks' + ], 0), + $stats + ); + } + + /** + * Helper for whether or not to trigger an error or just return false on an error. + * + * @param string $message The PHP error message to emit. + * @param int $flags Bitwise mask of options (STREAM_REPORT_ERRORS) + * @return bool Returns false + */ + private function returnError($message, $flags) + { + if ($flags & STREAM_REPORT_ERRORS) { + trigger_error($message, E_USER_WARNING); + } + return false; + } + + /** + * Helper for determining which predefinedAcl to use given a mode. + * + * @param int $mode Decimal representation of the file system permissions + * @return string + */ + private function determineAclFromMode($mode) + { + if ($mode & 0004) { + // If any user can read, assume it should be publicRead. + return 'publicRead'; + } elseif ($mode & 0040) { + // If any group user can read, assume it should be projectPrivate. + return 'projectPrivate'; + } else { + // Otherwise, assume only the project/bucket owner can use the bucket. + return 'private'; + } + } +} diff --git a/src/Storage/WriteStream.php b/src/Storage/WriteStream.php new file mode 100644 index 000000000000..e3dcd689393c --- /dev/null +++ b/src/Storage/WriteStream.php @@ -0,0 +1,110 @@ +setUploader($uploader); + } + if (array_key_exists('chunkSize', $options)) { + $this->chunkSize = $options['chunkSize']; + } + $this->stream = new BufferStream($this->chunkSize); + } + + /** + * Close the stream. Uploads any remaining data. + */ + public function close() + { + if ($this->uploader && $this->hasWritten) { + $this->uploader->upload(); + $this->uploader = null; + } + } + + /** + * Write to the stream. If we pass the chunkable size, upload the available chunk. + * + * @param string $data Data to write + * @return int The number of bytes written + * @throws \RuntimeException + */ + public function write($data) + { + if (!isset($this->uploader)) { + throw new \RuntimeException("No uploader set."); + } + + // Ensure we have a resume uri here because we need to create the streaming + // upload before we have data (size of 0). + $this->uploader->getResumeUri(); + $this->hasWritten = true; + + if (!$this->stream->write($data)) { + $this->uploader->upload($this->getChunkedWriteSize()); + } + return strlen($data); + } + + /** + * Set the uploader for this class. You may need to set this after initialization + * if the uploader depends on this stream. + * + * @param AbstractUploader $uploader The new uploader to use. + */ + public function setUploader($uploader) + { + $this->uploader = $uploader; + } + + private function getChunkedWriteSize() + { + return (int) floor($this->getSize() / $this->chunkSize) * $this->chunkSize; + } +} diff --git a/src/Upload/ResumableUploader.php b/src/Upload/ResumableUploader.php index dea4dcb44e59..69666282c233 100644 --- a/src/Upload/ResumableUploader.php +++ b/src/Upload/ResumableUploader.php @@ -31,7 +31,7 @@ class ResumableUploader extends AbstractUploader /** * @var int */ - private $rangeStart = 0; + protected $rangeStart = 0; /** * @var string diff --git a/src/Upload/StreamableUploader.php b/src/Upload/StreamableUploader.php new file mode 100644 index 000000000000..70013d84f227 --- /dev/null +++ b/src/Upload/StreamableUploader.php @@ -0,0 +1,85 @@ +getResumeUri(); + + if ($writeSize) { + $rangeEnd = $this->rangeStart + $writeSize - 1; + $data = $this->data->read($writeSize); + } else { + $rangeEnd = '*'; + $data = $this->data; + } + + // do the streaming write + $headers = [ + 'Content-Length' => $writeSize, + 'Content-Type' => $this->contentType, + 'Content-Range' => "bytes {$this->rangeStart}-$rangeEnd/*" + ]; + + $request = new Request( + 'PUT', + $resumeUri, + $headers, + $data + ); + + try { + $response = $this->requestWrapper->send($request, $this->requestOptions); + } catch (ServiceException $ex) { + throw new GoogleException( + "Upload failed. Please use this URI to resume your upload: $resumeUri", + $ex->getCode(), + $ex + ); + } + + // reset the buffer with the remaining contents + $this->rangeStart += $writeSize; + + return json_decode($response->getBody(), true); + } +} diff --git a/tests/system/Storage/StorageTestCase.php b/tests/system/Storage/StorageTestCase.php index 2cc5c72f15a5..c41dbd700c54 100644 --- a/tests/system/Storage/StorageTestCase.php +++ b/tests/system/Storage/StorageTestCase.php @@ -39,7 +39,8 @@ public static function setUpBeforeClass() self::$client = new StorageClient([ 'keyFilePath' => getenv('GOOGLE_CLOUD_PHP_TESTS_KEY_PATH') ]); - self::$bucket = self::$client->createBucket(uniqid(self::TESTING_PREFIX)); + $bucket = getenv('BUCKET') ?: uniqid(self::TESTING_PREFIX); + self::$bucket = self::$client->createBucket($bucket); self::$object = self::$bucket->upload('somedata', ['name' => uniqid(self::TESTING_PREFIX)]); self::$hasSetUp = true; } @@ -62,5 +63,3 @@ public static function tearDownFixtures() } } } - - diff --git a/tests/system/Storage/StreamWrapper/DirectoryTest.php b/tests/system/Storage/StreamWrapper/DirectoryTest.php new file mode 100644 index 000000000000..a7e1883430af --- /dev/null +++ b/tests/system/Storage/StreamWrapper/DirectoryTest.php @@ -0,0 +1,84 @@ +upload('somedata', ['name' => 'some_folder/1.txt']); + self::$bucket->upload('somedata', ['name' => 'some_folder/2.txt']); + self::$bucket->upload('somedata', ['name' => 'some_folder/3.txt']); + } + + public function testMkDir() + { + $dir = self::generateUrl('test_directory'); + $this->assertTrue(mkdir($dir)); + $this->assertTrue(file_exists($dir . '/')); + $this->assertTrue(is_dir($dir . '/')); + } + + public function testRmDir() + { + $dir = self::generateUrl('test_directory/'); + $this->assertTrue(rmdir($dir)); + $this->assertFalse(file_exists($dir . '/')); + } + + public function testMkDirCreatesBucket() + { + $newBucket = uniqid(self::TESTING_PREFIX); + $bucketUrl = "gs://$newBucket/"; + $this->assertTrue(mkdir($bucketUrl, 0700)); + + $bucket = self::$client->bucket($newBucket); + $this->assertTrue($bucket->exists()); + $this->assertTrue(rmdir($bucketUrl)); + } + + public function testListDirectory() + { + $dir = self::generateUrl('some_folder'); + $fd = opendir($dir); + $this->assertEquals('some_folder/1.txt', readdir($fd)); + $this->assertEquals('some_folder/2.txt', readdir($fd)); + rewinddir($fd); + $this->assertEquals('some_folder/1.txt', readdir($fd)); + closedir($fd); + } + + public function testScanDirectory() + { + $dir = self::generateUrl('some_folder'); + $expected = [ + 'some_folder/1.txt', + 'some_folder/2.txt', + 'some_folder/3.txt', + ]; + $this->assertEquals($expected, scandir($dir)); + $this->assertEquals(array_reverse($expected), scandir($dir, SCANDIR_SORT_DESCENDING)); + } +} diff --git a/tests/system/Storage/StreamWrapper/ImageTest.php b/tests/system/Storage/StreamWrapper/ImageTest.php new file mode 100644 index 000000000000..9ea0769db235 --- /dev/null +++ b/tests/system/Storage/StreamWrapper/ImageTest.php @@ -0,0 +1,78 @@ +upload( + $contents, + ['name' => 'exif.jpg'] + ); + $contents = file_get_contents(self::TEST_IMAGE); + self::$bucket->upload( + $contents, + ['name' => 'plain.jpg'] + ); + } + + /** + * @dataProvider imageProvider + */ + public function testGetImageSize($image, $width, $height) + { + $url = self::generateUrl($image); + $size = getimagesize($url); + $this->assertEquals($width, $size[0]); + $this->assertEquals($height, $size[1]); + } + + /** + * @dataProvider imageProvider + */ + public function testGetImageSizeWithInfo($image, $width, $height) + { + $url = self::generateUrl($image); + $info = array(); + $size = getimagesize($url, $info); + $this->assertEquals($width, $size[0]); + $this->assertEquals($height, $size[1]); + $this->assertTrue(count(array_keys($info)) > 1); + } + + public function imageProvider() + { + return [ + ['plain.jpg', 1956, 960], + ['exif.jpg', 3960, 2640], + ]; + } +} diff --git a/tests/system/Storage/StreamWrapper/ReadTest.php b/tests/system/Storage/StreamWrapper/ReadTest.php new file mode 100644 index 000000000000..6f9dbd619aee --- /dev/null +++ b/tests/system/Storage/StreamWrapper/ReadTest.php @@ -0,0 +1,63 @@ +file = self::generateUrl(self::$object->name()); + } + + public function testFread() + { + $fd = fopen($this->file, 'r'); + $expected = 'somedata'; + $this->assertEquals($expected, fread($fd, strlen($expected))); + $this->assertTrue(fclose($fd)); + } + + public function testFileGetContents() + { + $this->assertEquals('somedata', file_get_contents($this->file)); + } + + public function testGetLines() + { + $fd = fopen($this->file, 'r'); + $expected = 'somedata'; + $this->assertEquals($expected, fgets($fd)); + $this->assertTrue(fclose($fd)); + } + + public function testEof() + { + $fd = fopen($this->file, 'r'); + $this->assertFalse(feof($fd)); + fread($fd, 1000); + $this->assertTrue(feof($fd)); + $this->assertTrue(fclose($fd)); + } + +} diff --git a/tests/system/Storage/StreamWrapper/RenameTest.php b/tests/system/Storage/StreamWrapper/RenameTest.php new file mode 100644 index 000000000000..9ea898393fd6 --- /dev/null +++ b/tests/system/Storage/StreamWrapper/RenameTest.php @@ -0,0 +1,54 @@ +upload('somedata', ['name' => self::TEST_FILE]); + } + + public function testRenameFile() + { + $oldFile = self::generateUrl(self::TEST_FILE); + $newFile = self::generateUrl(self::NEW_TEST_FILE); + $this->assertTrue(rename($oldFile, $newFile)); + $this->assertTrue(file_exists($newFile)); + } + + public function testRenameDirectory() + { + $oldFolder = self::generateUrl(dirname(self::TEST_FILE)); + $newFolder = self::generateUrl('new_folder'); + $newFile = $newFolder . '/bar.txt'; + $this->assertTrue(rename($oldFolder, $newFolder)); + $this->assertTrue(file_exists($newFile)); + } + +} diff --git a/tests/system/Storage/StreamWrapper/StreamWrapperTestCase.php b/tests/system/Storage/StreamWrapper/StreamWrapperTestCase.php new file mode 100644 index 000000000000..8928ff7da42b --- /dev/null +++ b/tests/system/Storage/StreamWrapper/StreamWrapperTestCase.php @@ -0,0 +1,46 @@ +registerStreamWrapper(); + } + + public static function tearDownAfterClass() + { + self::$client->unregisterStreamWrapper(); + parent::tearDownAfterClass(); + } + + protected static function generateUrl($file) + { + $bucketName = self::$bucket->name(); + return "gs://$bucketName/$file"; + } + +} diff --git a/tests/system/Storage/StreamWrapper/UrlStatTest.php b/tests/system/Storage/StreamWrapper/UrlStatTest.php new file mode 100644 index 000000000000..aefa2d8bc6c0 --- /dev/null +++ b/tests/system/Storage/StreamWrapper/UrlStatTest.php @@ -0,0 +1,107 @@ +name()); + self::$dirUrl = self::generateUrl('some_folder/'); + mkdir(self::$dirUrl); + } + + public function testUrlStatFile() + { + $stat = stat(self::$fileUrl); + $this->assertEquals(33206, $stat['mode']); + } + + public function testUrlStatDirectory() + { + $stat = stat(self::$dirUrl); + $this->assertEquals(16895, $stat['mode']); + } + + public function testStatOnOpenFileForWrite() + { + $fd = fopen(self::$fileUrl, 'w'); + $stat = fstat($fd); + $this->assertEquals(33206, $stat['mode']); + } + + public function testStatOnOpenFileForRead() + { + $fd = fopen(self::$fileUrl, 'r'); + $stat = fstat($fd); + $this->assertEquals(33060, $stat['mode']); + } + + public function testIsWritable() + { + $this->assertTrue(is_writable(self::$dirUrl)); + $this->assertTrue(is_writable(self::$fileUrl)); + } + + public function testIsReadable() + { + $this->assertTrue(is_readable(self::$dirUrl)); + $this->assertTrue(is_readable(self::$fileUrl)); + } + + public function testFileExists() + { + $this->assertTrue(file_exists(self::$dirUrl)); + $this->assertTrue(file_exists(self::$fileUrl)); + } + + public function testIsLink() + { + $this->assertFalse(is_link(self::$dirUrl)); + $this->assertFalse(is_link(self::$fileUrl)); + } + + public function testIsExecutable() + { + // php returns false for is_executable if the file is a directory + // https://github.com/php/php-src/blob/master/ext/standard/filestat.c#L907 + $this->assertFalse(is_executable(self::$dirUrl)); + $this->assertFalse(is_executable(self::$fileUrl)); + } + + public function testIsFile() + { + $this->assertTrue(is_file(self::$fileUrl)); + $this->assertFalse(is_file(self::$dirUrl)); + } + + public function testIsDir() + { + $this->assertTrue(is_dir(self::$dirUrl)); + $this->assertFalse(is_dir(self::$fileUrl)); + } + +} diff --git a/tests/system/Storage/StreamWrapper/WriteTest.php b/tests/system/Storage/StreamWrapper/WriteTest.php new file mode 100644 index 000000000000..74d3bb45c6f9 --- /dev/null +++ b/tests/system/Storage/StreamWrapper/WriteTest.php @@ -0,0 +1,73 @@ +fileUrl = self::generateUrl('output.txt'); + unlink($this->fileUrl); + } + + public function tearDown() + { + unlink($this->fileUrl); + } + + public function testFilePutContents() + { + $this->assertFalse(file_exists($this->fileUrl)); + + $output = 'This is a test'; + $this->assertEquals(strlen($output), file_put_contents($this->fileUrl, $output)); + + $this->assertTrue(file_exists($this->fileUrl)); + } + + public function testFwrite() + { + $this->assertFalse(file_exists($this->fileUrl)); + + $output = 'This is a test'; + $fd = fopen($this->fileUrl, 'w'); + $this->assertEquals(strlen($output), fwrite($fd, $output)); + $this->assertTrue(fclose($fd)); + + $this->assertTrue(file_exists($this->fileUrl)); + } + + public function testStreamingWrite() + { + $this->assertFalse(file_exists($this->fileUrl)); + + $fp = fopen($this->fileUrl, 'w'); + for($i = 0; $i < 20000; $i++) { + fwrite($fp, "Line Number: $i\n"); + } + fclose($fp); + + $this->assertTrue(file_exists($this->fileUrl)); + } +} diff --git a/tests/unit/Storage/BucketTest.php b/tests/unit/Storage/BucketTest.php index 4e50e91f8b96..0d0bd2558e86 100644 --- a/tests/unit/Storage/BucketTest.php +++ b/tests/unit/Storage/BucketTest.php @@ -18,6 +18,8 @@ namespace Google\Cloud\Tests\Storage; use Google\Cloud\Exception\NotFoundException; +use Google\Cloud\Exception\ServerException; +use Google\Cloud\Exception\ServiceException; use Google\Cloud\Storage\Bucket; use Google\Cloud\Storage\Connection\ConnectionInterface; use Google\Cloud\Storage\StorageObject; @@ -311,4 +313,31 @@ public function testGetsName() $this->assertEquals($name, $bucket->name()); } + + public function testIsWritable() + { + $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader); + $this->resumableUploader->getResumeUri()->willReturn('http://some-uri/'); + $bucket = new Bucket($this->connection->reveal(), $name = 'bucket'); + $this->assertTrue($bucket->isWritable()); + } + + public function testIsWritableAccessDenied() + { + $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader); + $this->resumableUploader->getResumeUri()->willThrow(new ServiceException('access denied', 403)); + $bucket = new Bucket($this->connection->reveal(), $name = 'bucket'); + $this->assertFalse($bucket->isWritable()); + } + + /** + * @expectedException \Google\Cloud\Exception\ServerException + */ + public function testIsWritableServerException() + { + $this->connection->insertObject(Argument::any())->willReturn($this->resumableUploader); + $this->resumableUploader->getResumeUri()->willThrow(new ServerException('maintainence')); + $bucket = new Bucket($this->connection->reveal(), $name = 'bucket'); + $bucket->isWritable(); // raises exception + } } diff --git a/tests/unit/Storage/ReadStreamTest.php b/tests/unit/Storage/ReadStreamTest.php new file mode 100644 index 000000000000..0b64fde0d9b3 --- /dev/null +++ b/tests/unit/Storage/ReadStreamTest.php @@ -0,0 +1,71 @@ +prophesize('Psr\Http\Message\StreamInterface'); + $httpStream->getSize()->willReturn(null); + $httpStream->getMetadata('wrapper_data')->willReturn([ + "Foo: bar", + "User-Agent: php", + "Content-Length: 1234", + "Asdf: qwer", + ]); + + $stream = new ReadStream($httpStream->reveal()); + + $this->assertEquals(1234, $stream->getSize()); + } + + public function testReadsFromHeadersWhenGetSizeIsZero() + { + $httpStream = $this->prophesize('Psr\Http\Message\StreamInterface'); + $httpStream->getSize()->willReturn(0); + $httpStream->getMetadata('wrapper_data')->willReturn([ + "Foo: bar", + "User-Agent: php", + "Content-Length: 1234", + "Asdf: qwer", + ]); + + $stream = new ReadStream($httpStream->reveal()); + + $this->assertEquals(1234, $stream->getSize()); + } + + public function testNoContentLengthHeader() + { + $httpStream = $this->prophesize('Psr\Http\Message\StreamInterface'); + $httpStream->getSize()->willReturn(null); + $httpStream->getMetadata('wrapper_data')->willReturn(array()); + + $stream = new ReadStream($httpStream->reveal()); + + $this->assertEquals(0, $stream->getSize()); + } +} diff --git a/tests/unit/Storage/StorageClientTest.php b/tests/unit/Storage/StorageClientTest.php index 97881840d082..96b3f3d7ac7f 100644 --- a/tests/unit/Storage/StorageClientTest.php +++ b/tests/unit/Storage/StorageClientTest.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Tests\Storage; use Google\Cloud\Storage\StorageClient; +use Google\Cloud\Storage\StreamWrapper; use Prophecy\Argument; /** @@ -82,6 +83,14 @@ public function testCreatesBucket() $this->assertInstanceOf('Google\Cloud\Storage\Bucket', $this->client->createBucket('bucket')); } + + public function testRegisteringStreamWrapper() + { + $this->assertTrue($this->client->registerStreamWrapper()); + $this->assertEquals($this->client, StreamWrapper::getClient()); + $this->assertTrue(in_array('gs', stream_get_wrappers())); + $this->client->unregisterStreamWrapper(); + } } class StorageTestClient extends StorageClient diff --git a/tests/unit/Storage/StreamWrapperTest.php b/tests/unit/Storage/StreamWrapperTest.php new file mode 100644 index 000000000000..95a4b3148192 --- /dev/null +++ b/tests/unit/Storage/StreamWrapperTest.php @@ -0,0 +1,426 @@ +client = $this->prophesize(StorageClient::class); + $this->bucket = $this->prophesize(Bucket::class); + $this->client->bucket('my_bucket')->willReturn($this->bucket->reveal()); + + StreamWrapper::register($this->client->reveal()); + } + + public function tearDown() + { + StreamWrapper::unregister(); + + parent::tearDown(); + } + + /** + * @group storageRead + */ + public function testOpeningExistingFile() + { + $this->mockObjectData("existing_file.txt", "some data to read"); + + $fp = fopen('gs://my_bucket/existing_file.txt', 'r'); + $this->assertEquals("some da", fread($fp, 7)); + $this->assertEquals("ta to read", fread($fp, 1000)); + fclose($fp); + } + + /** + * @group storageRead + */ + public function testOpeningNonExistentFileReturnsFalse() + { + $this->mockDownloadException('non-existent/file.txt', \Google\Cloud\Exception\NotFoundException::class); + + $fp = @fopen('gs://my_bucket/non-existent/file.txt', 'r'); + $this->assertFalse($fp); + } + + /** + * @group storageRead + */ + public function testUnknownOpenMode() + { + $fp = @fopen('gs://my_bucket/existing_file.txt', 'a'); + $this->assertFalse($fp); + } + + /** + * @group storageRead + */ + public function testFileGetContents() + { + $this->mockObjectData("file_get_contents.txt", "some data to read"); + + $this->assertEquals('some data to read', file_get_contents('gs://my_bucket/file_get_contents.txt')); + } + + /** + * @group storageRead + */ + public function testReadLines() + { + $this->mockObjectData("some_long_file.txt", "line1.\nline2."); + + $fp = fopen('gs://my_bucket/some_long_file.txt', 'r'); + $this->assertEquals("line1.\n", fgets($fp)); + $this->assertEquals("line2.", fgets($fp)); + fclose($fp); + } + + /** + * @group storageWrite + */ + public function testFileWrite() + { + $uploader = $this->prophesize(StreamableUploader::class); + $uploader->upload()->shouldBeCalled(); + $uploader->getResumeUri()->willReturn('https://resume-uri/'); + $this->bucket->getStreamableUploader("", Argument::type('array'))->willReturn($uploader->reveal()); + + $fp = fopen('gs://my_bucket/output.txt', 'w'); + $this->assertEquals(6, fwrite($fp, "line1.")); + $this->assertEquals(6, fwrite($fp, "line2.")); + fclose($fp); + } + + /** + * @group storageWrite + */ + public function testFilePutContents() + { + $uploader = $this->prophesize(StreamableUploader::class); + $uploader->upload()->shouldBeCalled(); + $uploader->getResumeUri()->willReturn('https://resume-uri/'); + $this->bucket->getStreamableUploader("", Argument::type('array'))->willReturn($uploader->reveal()); + + file_put_contents('gs://my_bucket/file_put_contents.txt', 'Some data.'); + } + + /** + * @group storageSeek + */ + public function testSeekOnWritableStream() + { + $uploader = $this->prophesize(StreamableUploader::class); + $this->bucket->getStreamableUploader("", Argument::type('array'))->willReturn($uploader->reveal()); + + $fp = fopen('gs://my_bucket/output.txt', 'w'); + $this->assertEquals(-1, fseek($fp, 100)); + fclose($fp); + } + + /** + * @group storageSeek + */ + public function testSeekOnReadableStream() + { + $this->mockObjectData("some_long_file.txt", "line1.\nline2."); + $fp = fopen('gs://my_bucket/some_long_file.txt', 'r'); + $this->assertEquals(-1, fseek($fp, 100)); + fclose($fp); + } + + /** + * @group storageInfo + */ + public function testFstat() + { + $this->mockObjectData("some_long_file.txt", "line1.\nline2."); + $fp = fopen('gs://my_bucket/some_long_file.txt', 'r'); + $stat = fstat($fp); + $this->assertEquals(33206, $stat['mode']); + fclose($fp); + } + + /** + * @group storageInfo + */ + public function testStat() + { + $object = $this->prophesize(StorageObject::class); + $object->info()->willReturn([ + 'size' => 1234, + 'updated' => '2017-01-19T19:31:35.833Z', + 'timeCreated' => '2017-01-19T19:31:35.833Z' + ]); + $this->bucket->object('some_long_file.txt')->willReturn($object->reveal()); + $this->bucket->isWritable()->willReturn(true); + + $stat = stat('gs://my_bucket/some_long_file.txt'); + $this->assertEquals(33206, $stat['mode']); + } + + /** + * @group storageInfo + * @expectedException PHPUnit_Framework_Error_Warning + */ + public function testStatOnNonExistentFile() + { + $object = $this->prophesize(StorageObject::class); + $object->info()->willThrow(NotFoundException::class); + $this->bucket->object('non-existent/file.txt')->willReturn($object->reveal()); + + stat('gs://my_bucket/non-existent/file.txt'); + } + + /** + * @group storageDelete + */ + public function testUnlink() + { + $obj = $this->prophesize(StorageObject::class); + $obj->delete()->willReturn(true)->shouldBeCalled(); + $this->bucket->object('some_long_file.txt')->willReturn($obj->reveal()); + $this->assertTrue(unlink('gs://my_bucket/some_long_file.txt')); + } + + /** + * @group storageDelete + */ + public function testUnlinkOnNonExistentFile() + { + $obj = $this->prophesize(StorageObject::class); + $obj->delete()->willThrow(\Google\Cloud\Exception\NotFoundException::class); + $this->bucket->object('some_long_file.txt')->willReturn($obj->reveal()); + $this->assertFalse(unlink('gs://my_bucket/some_long_file.txt')); + } + + /** + * @group storageDirectory + */ + public function testMkdir() + { + $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'publicRead'])->shouldBeCalled(); + $this->assertTrue(mkdir('gs://my_bucket/foo/bar')); + } + + /** + * @group storageDirectory + */ + public function testMkdirProjectPrivate() + { + $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'projectPrivate'])->shouldBeCalled(); + $this->assertTrue(mkdir('gs://my_bucket/foo/bar', 0740)); + } + + /** + * @group storageDirectory + */ + public function testMkdirPrivate() + { + $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'private'])->shouldBeCalled(); + $this->assertTrue(mkdir('gs://my_bucket/foo/bar', 0700)); + } + + /** + * @group storageDirectory + */ + public function testMkdirOnBadDirectory() + { + $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'publicRead'])->willThrow(\Google\Cloud\Exception\NotFoundException::class); + $this->assertFalse(mkdir('gs://my_bucket/foo/bar')); + } + + /** + * @group storageDirectory + */ + public function testMkDirCreatesBucket() + { + $this->bucket->exists()->willReturn(false); + $this->bucket->name()->willReturn('my_bucket'); + $this->client->createBucket('my_bucket', [ + 'predefinedAcl' => 'publicRead', + 'predefinedDefaultObjectAcl' => 'publicRead'] + )->willReturn($this->bucket); + $this->bucket->upload('', ['name' => 'foo/bar/', 'predefinedAcl' => 'publicRead'])->shouldBeCalled(); + + $this->assertTrue(mkdir('gs://my_bucket/foo/bar', 0777, STREAM_MKDIR_RECURSIVE)); + } + + /** + * @group storageDirectory + */ + public function testRmdir() + { + $obj = $this->prophesize(StorageObject::class); + $obj->delete()->willReturn(true)->shouldBeCalled(); + $this->bucket->object('foo/bar/')->willReturn($obj->reveal()); + $this->assertTrue(rmdir('gs://my_bucket/foo/bar')); + } + + /** + * @group storageDirectory + */ + public function testRmdirOnBadDirectory() + { + $obj = $this->prophesize(StorageObject::class); + $obj->delete()->willThrow(\Google\Cloud\Exception\NotFoundException::class); + $this->bucket->object('foo/bar/')->willReturn($obj->reveal()); + $this->assertFalse(rmdir('gs://my_bucket/foo/bar')); + } + + /** + * @group storageDirectory + */ + public function testDirectoryListing() + { + $this->mockDirectoryListing('foo/', ['foo/file1.txt', 'foo/file2.txt', 'foo/file3.txt', 'foo/file4.txt']); + $fd = opendir('gs://my_bucket/foo/'); + $this->assertEquals('foo/file1.txt', readdir($fd)); + $this->assertEquals('foo/file2.txt', readdir($fd)); + $this->assertEquals('foo/file3.txt', readdir($fd)); + rewinddir($fd); + $this->assertEquals('foo/file1.txt', readdir($fd)); + closedir($fd); + } + + /** + * @group storageDirectory + */ + public function testDirectoryListingViaScan() + { + $files = ['foo/file1.txt', 'foo/file2.txt', 'foo/file3.txt', 'foo/file4.txt']; + $this->mockDirectoryListing('foo/', $files); + $this->assertEquals($files, scandir('gs://my_bucket/foo/')); + } + + public function testRenameFile() + { + $this->mockDirectoryListing('foo.txt', ['foo.txt']); + $object = $this->prophesize(StorageObject::class); + $object->rename('new_location/foo.txt', ['destinationBucket' => 'my_bucket'])->shouldBeCalled(); + $this->bucket->object('foo.txt')->willReturn($object->reveal()); + + $this->assertTrue(rename('gs://my_bucket/foo.txt', 'gs://my_bucket/new_location/foo.txt')); + } + + public function testRenameToDifferentBucket() + { + $this->mockDirectoryListing('foo.txt', ['foo.txt']); + $object = $this->prophesize(StorageObject::class); + $object->rename('bar/foo.txt', ['destinationBucket' => 'another_bucket'])->shouldBeCalled(); + $this->bucket->object('foo.txt')->willReturn($object->reveal()); + + $this->assertTrue(rename('gs://my_bucket/foo.txt', 'gs://another_bucket/bar/foo.txt')); + } + + public function testRenameDirectory() + { + $this->mockDirectoryListing('foo', ['foo/bar1.txt', 'foo/bar2.txt', 'foo/asdf/bar.txt']); + + $object = $this->prophesize(StorageObject::class); + $object->rename('nested/folder/bar1.txt', ['destinationBucket' => 'another_bucket'])->shouldBeCalled(); + $this->bucket->object('foo/bar1.txt')->willReturn($object->reveal()); + + $object = $this->prophesize(StorageObject::class); + $object->rename('nested/folder/bar2.txt', ['destinationBucket' => 'another_bucket'])->shouldBeCalled(); + $this->bucket->object('foo/bar2.txt')->willReturn($object->reveal()); + + $object = $this->prophesize(StorageObject::class); + $object->rename('nested/folder/asdf/bar.txt', ['destinationBucket' => 'another_bucket'])->shouldBeCalled(); + $this->bucket->object('foo/asdf/bar.txt')->willReturn($object->reveal()); + + $this->assertTrue(rename('gs://my_bucket/foo', 'gs://another_bucket/nested/folder')); + } + + public function testCanSpecifyChunkSizeViaContext() + { + + $uploader = $this->prophesize(StreamableUploader::class); + $upload = $uploader->upload(5)->willReturn(array())->shouldBeCalled(); + $uploader->upload()->shouldBeCalled(); + $uploader->getResumeUri()->willReturn('https://resume-uri/'); + $this->bucket->getStreamableUploader("", Argument::type('array'))->willReturn($uploader->reveal()); + + $context = stream_context_create(array( + 'gs' => array( + 'chunkSize' => 5 + ) + )); + $fp = fopen('gs://my_bucket/existing_file.txt', 'w', false, $context); + $this->assertEquals(9, fwrite($fp, "123456789")); + fclose($fp); + } + + private function mockObjectData($file, $data, $bucket = null) + { + $bucket = $bucket ?: $this->bucket; + $stream = new \GuzzleHttp\Psr7\BufferStream(100); + $stream->write($data); + $object = $this->prophesize(StorageObject::class); + $object->downloadAsStream(Argument::any())->willReturn($stream); + $bucket->object($file)->willReturn($object->reveal()); + } + + private function mockDownloadException($file, $exception) + { + $object = $this->prophesize(StorageObject::class); + $object->downloadAsStream(Argument::any())->willThrow($exception); + $this->bucket->object($file)->willReturn($object->reveal()); + } + + private function mockDirectoryListing($path, $filesToReturn) + { + $test = $this; + $this->bucket->objects( + Argument::that(function($options) use ($path) { + return $options['prefix'] == $path; + }) + )->will(function() use ($test, $filesToReturn) { + return $test->fileListGenerator($filesToReturn); + }); + } + + private function fileListGenerator($fileToReturn) + { + foreach($fileToReturn as $file) { + $obj = $this->prophesize(StorageObject::class); + $obj->name()->willReturn($file); + yield $obj->reveal(); + } + } +} diff --git a/tests/unit/Storage/WriteStreamTest.php b/tests/unit/Storage/WriteStreamTest.php new file mode 100644 index 000000000000..95c706b33b8f --- /dev/null +++ b/tests/unit/Storage/WriteStreamTest.php @@ -0,0 +1,56 @@ +prophesize(StreamableUploader::class); + $uploader->getResumeUri()->willReturn('https://some-resume-uri/'); + $stream = new WriteStream($uploader->reveal(), ['chunkSize' => 10]); + + // We should see 2 calls to upload with size of 10. + $upload = $uploader->upload(10)->will(function($args) use ($stream) { + if (count($args) > 0) { + $size = $args[0]; + $stream->read(10); + } + return array(); + }); + + // We should see a single call to finish the upload. + $uploader->upload()->shouldBeCalledTimes(1); + + $stream->write('1234567'); + $upload->shouldHaveBeenCalledTimes(0); + $stream->write('8901234'); + $upload->shouldHaveBeenCalledTimes(1); + $stream->write('5678901'); + $upload->shouldHaveBeenCalledTimes(2); + $stream->close(); + } +} diff --git a/tests/unit/Upload/StreamableUploaderTest.php b/tests/unit/Upload/StreamableUploaderTest.php new file mode 100644 index 000000000000..f162e0a2a8f3 --- /dev/null +++ b/tests/unit/Upload/StreamableUploaderTest.php @@ -0,0 +1,147 @@ +requestWrapper = $this->prophesize('Google\Cloud\RequestWrapper'); + $this->stream = new WriteStream(null, ['chunkSize' => 16]); + $this->successBody = '{"canI":"kickIt"}'; + } + + public function testStreamingWrites() + { + $resumeResponse = new Response(200, ['Location' => 'http://some-resume-uri.example.com'], $this->successBody); + $this->requestWrapper->send( + Argument::that(function($request){ + return (string) $request->getUri() == 'http://www.example.com'; + }), + Argument::type('array') + )->willReturn($resumeResponse); + + $uploadResponse = new Response(200, [], $this->successBody); + $upload = $this->requestWrapper->send( + Argument::that(function($request){ + return (string) $request->getUri() == 'http://some-resume-uri.example.com'; + }), + Argument::type('array') + )->willReturn($uploadResponse); + + $uploader = new StreamableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com', + ['chunkSize' => 16] + ); + $this->stream->setUploader($uploader); + + // write some data smaller than the chunk size + $this->stream->write("0123456789"); + $upload->shouldHaveBeenCalledTimes(0); + + // write some more data that will put us over the chunk size. + $this->stream->write("more text"); + $upload->shouldHaveBeenCalledTimes(1); + + // finish the upload + $this->assertEquals(json_decode($this->successBody, true), $uploader->upload()); + $upload->shouldHaveBeenCalledTimes(2); + } + + public function testUploadsData() + { + $response = new Response(200, ['Location' => 'theResumeUri'], $this->successBody); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->willReturn($response); + + $uploader = new StreamableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com' + ); + $this->stream->setUploader($uploader); + + $this->assertEquals(json_decode($this->successBody, true), $uploader->upload()); + } + + public function testGetResumeUri() + { + $resumeUri = 'theResumeUri'; + $response = new Response(200, ['Location' => $resumeUri]); + + $this->requestWrapper->send( + Argument::type('Psr\Http\Message\RequestInterface'), + Argument::type('array') + )->willReturn($response); + + $uploader = new StreamableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com' + ); + $this->stream->setUploader($uploader); + + $this->assertEquals($resumeUri, $uploader->getResumeUri()); + } + + /** + * @expectedException Google\Cloud\Exception\GoogleException + */ + public function testThrowsExceptionWithFailedUpload() + { + $resumeUriResponse = new Response(200, ['Location' => 'theResumeUri']); + + $this->requestWrapper->send( + Argument::which('getMethod', 'POST'), + Argument::type('array') + )->willReturn($resumeUriResponse); + + $this->requestWrapper->send( + Argument::which('getMethod', 'PUT'), + Argument::type('array') + )->willThrow('Google\Cloud\Exception\GoogleException'); + + $uploader = new StreamableUploader( + $this->requestWrapper->reveal(), + $this->stream, + 'http://www.example.com' + ); + + $uploader->upload(); + } +} From 447fbedf7dba98313f479e0bbdb165cc3000b6b9 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Wed, 22 Feb 2017 14:41:17 -0500 Subject: [PATCH 064/107] Omit `model` if not set (#344) --- src/Translate/TranslateClient.php | 12 ++++++++---- tests/unit/Translate/TranslateClientTest.php | 20 +++++++++++++++++++- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Translate/TranslateClient.php b/src/Translate/TranslateClient.php index 086674a32e8e..942f1d41efa1 100644 --- a/src/Translate/TranslateClient.php +++ b/src/Translate/TranslateClient.php @@ -203,7 +203,7 @@ public function translate($string, array $options = []) * either plain-text or HTML. Acceptable values are `html` or * `text`. **Defaults to** `"html"`. * @type string $model The model to use for the translation request. May - * be `nmt` or `base`. **Defaults to** an empty string. + * be `nmt` or `base`. **Defaults to** null. * } * @return array A set of translation results. Each result includes a * `source` key containing the detected or provided language of the @@ -213,15 +213,19 @@ public function translate($string, array $options = []) public function translateBatch(array $strings, array $options = []) { $options += [ - 'model' => '', + 'model' => null, ]; - $response = $this->connection->listTranslations($options + [ + $options = array_filter($options + [ 'q' => $strings, 'key' => $this->key, 'target' => $this->targetLanguage, 'model' => $options['model'] - ]); + ], function ($opt) { + return !is_null($opt); + }); + + $response = $this->connection->listTranslations($options); $translations = []; $strings = array_values($strings); diff --git a/tests/unit/Translate/TranslateClientTest.php b/tests/unit/Translate/TranslateClientTest.php index 57efdcf25e01..86808bf28c30 100644 --- a/tests/unit/Translate/TranslateClientTest.php +++ b/tests/unit/Translate/TranslateClientTest.php @@ -41,7 +41,7 @@ public function testWithNoKey() $client = new TranslateTestClient(); $this->connection->listTranslations(Argument::that(function($args) { - if (!is_null($args['key'])) { + if (isset($args['key'])) { return false; } @@ -53,6 +53,24 @@ public function testWithNoKey() $client->translate('foo'); } + public function testTranslateModel() + { + $this->connection->listTranslations(Argument::that(function ($args) { + if (isset($args['model'])) return false; + })); + + $this->client->setConnection($this->connection->reveal()); + + $this->client->translate('foo bar'); + + $this->connection->listTranslations(Argument::that(function ($args) { + if ($args['model'] !== 'base') return false; + })); + + $this->client->setConnection($this->connection->reveal()); + $this->client->translate('foo bar', ['model' => 'base']); + } + public function testTranslate() { $expected = $this->getTranslateExpectedData('translate', 'translated', 'en'); From a7bac23c526a5920134d909af80fcba4976276e1 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Wed, 22 Feb 2017 15:25:16 -0500 Subject: [PATCH 065/107] Prepare v0.21.0 (#345) --- docs/manifest.json | 1 + src/ServiceBuilder.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 09802d20c00b..f06414b4a87f 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -7,6 +7,7 @@ "matchPartialServiceId": true, "markdown": "php", "versions": [ + "v0.21.0", "v0.20.2", "v0.20.1", "v0.20.0", diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index d8df7682795c..e55c4a2b3d24 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -48,7 +48,7 @@ */ class ServiceBuilder { - const VERSION = '0.20.2'; + const VERSION = '0.21.0'; /** * @var array Configuration options to be used between clients. From d865d7ca94f2fd06232ec968179a7c8b24acf2d4 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 23 Feb 2017 13:51:13 -0500 Subject: [PATCH 066/107] Fix php notices in stream wrapper (#347) --- src/Storage/StreamWrapper.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Storage/StreamWrapper.php b/src/Storage/StreamWrapper.php index f5e9e83c8a10..b25e0a1c5312 100644 --- a/src/Storage/StreamWrapper.php +++ b/src/Storage/StreamWrapper.php @@ -592,9 +592,17 @@ private function urlStatFile() */ private function statsFromFileInfo(array &$info, array &$stats) { - $stats['size'] = (int) $info['size']; - $stats['mtime'] = strtotime($info['updated']); - $stats['ctime'] = strtotime($info['timeCreated']); + $stats['size'] = (isset($info['size'])) + ? (int) $info['size'] + : null; + + $stats['mtime'] = (isset($info['updated'])) + ? strtotime($info['updated']) + : null; + + $stats['ctime'] = (isset($info['timeCreated'])) + ? strtotime($info['timeCreated']) + : null; } /** From 68bbbc030c256bc30f9adfab1830eb6e462a0958 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 23 Feb 2017 15:14:16 -0500 Subject: [PATCH 067/107] Prepare v0.21.1 (#348) --- docs/manifest.json | 1 + src/ServiceBuilder.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index f06414b4a87f..5efcfca27413 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -7,6 +7,7 @@ "matchPartialServiceId": true, "markdown": "php", "versions": [ + "v0.21.1", "v0.21.0", "v0.20.2", "v0.20.1", diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index e55c4a2b3d24..097653507b88 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -48,7 +48,7 @@ */ class ServiceBuilder { - const VERSION = '0.21.0'; + const VERSION = '0.21.1'; /** * @var array Configuration options to be used between clients. From c12bc78b4143f4cf9556fad9fc74ec6700b3d981 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Sun, 26 Feb 2017 21:08:19 -0500 Subject: [PATCH 068/107] Ensure that parse_url provides required keys (#352) --- src/Storage/StreamWrapper.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Storage/StreamWrapper.php b/src/Storage/StreamWrapper.php index b25e0a1c5312..e2c0a9923a5c 100644 --- a/src/Storage/StreamWrapper.php +++ b/src/Storage/StreamWrapper.php @@ -381,7 +381,11 @@ public function mkdir($path, $mode, $options) */ public function rename($from, $to) { - $url = parse_url($to); + $url = (array) parse_url($to) + [ + 'path' => '', + 'host' => '' + ]; + $destinationBucket = $url['host']; $destinationPath = substr($url['path'], 1); @@ -487,7 +491,11 @@ public function url_stat($path, $flags) */ private function openPath($path) { - $url = parse_url($path); + $url = (array) parse_url($path) + [ + 'scheme' => '', + 'path' => '', + 'host' => '' + ]; $this->protocol = $url['scheme']; $this->file = ltrim($url['path'], '/'); $client = self::getClient($this->protocol); From 5670baf57158c4e38d8db4308958fb4c09948d01 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Sun, 26 Feb 2017 21:08:58 -0500 Subject: [PATCH 069/107] Bigquery updates (#350) * update insertRows to accept big query types and rows to return big query types * improve system test coverage --- src/BigQuery/BigQueryClient.php | 16 +- src/BigQuery/Dataset.php | 25 +- src/BigQuery/Table.php | 43 +- src/BigQuery/ValueMapper.php | 28 ++ tests/snippets/BigQuery/DatasetTest.php | 4 + tests/snippets/BigQuery/TableTest.php | 6 +- tests/system/BigQuery/BigQueryTestCase.php | 17 +- .../system/BigQuery/LoadDataAndQueryTest.php | 447 +++++++++++------- tests/system/data/table-data.csv | 2 - tests/system/data/table-data.json | 3 + tests/system/data/table-schema.json | 93 ++++ tests/unit/BigQuery/DatasetTest.php | 11 +- tests/unit/BigQuery/TableTest.php | 11 +- tests/unit/BigQuery/ValueMapperTest.php | 28 ++ 14 files changed, 524 insertions(+), 210 deletions(-) delete mode 100644 tests/system/data/table-data.csv create mode 100644 tests/system/data/table-data.json create mode 100644 tests/system/data/table-schema.json diff --git a/src/BigQuery/BigQueryClient.php b/src/BigQuery/BigQueryClient.php index 70c9aaba22b4..7daae138f322 100644 --- a/src/BigQuery/BigQueryClient.php +++ b/src/BigQuery/BigQueryClient.php @@ -404,7 +404,12 @@ public function jobs(array $options = []) */ public function dataset($id) { - return new Dataset($this->connection, $id, $this->projectId); + return new Dataset( + $this->connection, + $id, + $this->projectId, + $this->mapper + ); } /** @@ -445,6 +450,7 @@ public function datasets(array $options = []) $this->connection, $dataset['datasetReference']['datasetId'], $this->projectId, + $this->mapper, $dataset ); } @@ -487,7 +493,13 @@ public function createDataset($id, array $options = []) ] ] + $options); - return new Dataset($this->connection, $id, $this->projectId, $response); + return new Dataset( + $this->connection, + $id, + $this->projectId, + $this->mapper, + $response + ); } /** diff --git a/src/BigQuery/Dataset.php b/src/BigQuery/Dataset.php index fbc90ca4839e..ffd930a11fa7 100644 --- a/src/BigQuery/Dataset.php +++ b/src/BigQuery/Dataset.php @@ -41,6 +41,11 @@ class Dataset */ private $info; + /** + * @var ValueMapper Maps values between PHP and BigQuery. + */ + private $mapper; + /** * @param ConnectionInterface $connection Represents a connection to * BigQuery. @@ -48,10 +53,16 @@ class Dataset * @param string $projectId The project's ID. * @param array $info [optional] The dataset's metadata. */ - public function __construct(ConnectionInterface $connection, $id, $projectId, array $info = []) - { + public function __construct( + ConnectionInterface $connection, + $id, + $projectId, + ValueMapper $mapper, + array $info = [] + ) { $this->connection = $connection; $this->info = $info; + $this->mapper = $mapper; $this->identity = [ 'datasetId' => $id, 'projectId' => $projectId @@ -141,7 +152,13 @@ public function update(array $metadata, array $options = []) */ public function table($id) { - return new Table($this->connection, $id, $this->identity['datasetId'], $this->identity['projectId']); + return new Table( + $this->connection, + $id, + $this->identity['datasetId'], + $this->identity['projectId'], + $this->mapper + ); } /** @@ -182,6 +199,7 @@ public function tables(array $options = []) $table['tableReference']['tableId'], $this->identity['datasetId'], $this->identity['projectId'], + $this->mapper, $table ); } @@ -227,6 +245,7 @@ public function createTable($id, array $options = []) $id, $this->identity['datasetId'], $this->identity['projectId'], + $this->mapper, $response ); } diff --git a/src/BigQuery/Table.php b/src/BigQuery/Table.php index 1202226f66d7..0d9b7502af0a 100644 --- a/src/BigQuery/Table.php +++ b/src/BigQuery/Table.php @@ -45,18 +45,31 @@ class Table */ private $info; + /** + * @var ValueMapper Maps values between PHP and BigQuery. + */ + private $mapper; + /** * @param ConnectionInterface $connection Represents a connection to * BigQuery. * @param string $id The table's id. * @param string $datasetId The dataset's id. * @param string $projectId The project's id. + * @param ValueMapper $mapper Maps values between PHP and BigQuery. * @param array $info [optional] The table's metadata. */ - public function __construct(ConnectionInterface $connection, $id, $datasetId, $projectId, array $info = []) - { + public function __construct( + ConnectionInterface $connection, + $id, + $datasetId, + $projectId, + ValueMapper $mapper, + array $info = [] + ) { $this->connection = $connection; $this->info = $info; + $this->mapper = $mapper; $this->identity = [ 'tableId' => $id, 'datasetId' => $datasetId, @@ -137,7 +150,7 @@ public function update(array $metadata, array $options = []) * $rows = $table->rows(); * * foreach ($rows as $row) { - * echo $row['name']; + * echo $row['name'] . PHP_EOL; * } * ``` * @@ -150,6 +163,7 @@ public function update(array $metadata, array $options = []) * @type int $startIndex Zero-based index of the starting row. * } * @return \Generator + * @throws GoogleException */ public function rows(array $options = []) { @@ -163,14 +177,23 @@ public function rows(array $options = []) return; } - foreach ($response['rows'] as $rows) { - $row = []; + foreach ($response['rows'] as $row) { + $mergedRow = []; + + if ($row === null) { + continue; + } + + if (!array_key_exists('f', $row)) { + throw new GoogleException('Bad response - missing key "f" for a row.'); + } - foreach ($rows['f'] as $key => $field) { - $row[$schema[$key]['name']] = $field['v']; + foreach ($row['f'] as $key => $value) { + $fieldSchema = $schema[$key]; + $mergedRow[$fieldSchema['name']] = $this->mapper->fromBigQuery($value, $fieldSchema); } - yield $row; + yield $mergedRow; } $options['pageToken'] = isset($response['nextPageToken']) ? $response['nextPageToken'] : null; @@ -460,6 +483,10 @@ public function insertRows(array $rows, array $options = []) throw new \InvalidArgumentException('A row must have a data key.'); } + foreach ($row['data'] as $key => $item) { + $row['data'][$key] = $this->mapper->toBigQuery($item); + } + $row['json'] = $row['data']; unset($row['data']); $options['rows'][] = $row; diff --git a/src/BigQuery/ValueMapper.php b/src/BigQuery/ValueMapper.php index 0a2d26f65bb5..2d6bfe0b4815 100644 --- a/src/BigQuery/ValueMapper.php +++ b/src/BigQuery/ValueMapper.php @@ -45,6 +45,7 @@ class ValueMapper const TYPE_RECORD = 'RECORD'; const DATETIME_FORMAT = 'Y-m-d H:i:s.u'; + const DATETIME_FORMAT_INSERT = 'Y-m-d\TH:i:s.u'; /** * @var bool $returnInt64AsObject If true, 64 bit integers will be returned @@ -121,6 +122,33 @@ public function fromBigQuery(array $value, array $schema) } } + /** + * Maps a user provided value to the expected BigQuery format. + * + * @param mixed $value The value to map. + * @return mixed + */ + public function toBigQuery($value) + { + if ($value instanceof ValueInterface || $value instanceof Int64) { + return (string) $value; + } + + if ($value instanceof \DateTime) { + return $value->format(self::DATETIME_FORMAT_INSERT); + } + + if (is_array($value)) { + foreach ($value as $key => $item) { + $value[$key] = $this->toBigQuery($item); + } + + return $value; + } + + return $value; + } + /** * Maps a value to the expected parameter format. * diff --git a/tests/snippets/BigQuery/DatasetTest.php b/tests/snippets/BigQuery/DatasetTest.php index e72271cb9580..fb7d589504e0 100644 --- a/tests/snippets/BigQuery/DatasetTest.php +++ b/tests/snippets/BigQuery/DatasetTest.php @@ -20,6 +20,7 @@ use Google\Cloud\BigQuery\Connection\ConnectionInterface; use Google\Cloud\BigQuery\Dataset; use Google\Cloud\BigQuery\Table; +use Google\Cloud\BigQuery\ValueMapper; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Prophecy\Argument; @@ -30,9 +31,11 @@ class DatasetTest extends SnippetTestCase { private $identity; private $connection; + private $mapper; public function setUp() { + $this->mapper = new ValueMapper(false); $this->identity = ['datasetId' => 'id', 'projectId' => 'projectId']; $this->connection = $this->prophesize(ConnectionInterface::class); } @@ -43,6 +46,7 @@ public function getDataset($connection, array $info = []) $connection->reveal(), $this->identity['datasetId'], $this->identity['projectId'], + $this->mapper, $info ); } diff --git a/tests/snippets/BigQuery/TableTest.php b/tests/snippets/BigQuery/TableTest.php index a97c2978364d..5e05f6f1b526 100644 --- a/tests/snippets/BigQuery/TableTest.php +++ b/tests/snippets/BigQuery/TableTest.php @@ -22,6 +22,7 @@ use Google\Cloud\BigQuery\InsertResponse; use Google\Cloud\BigQuery\Job; use Google\Cloud\BigQuery\Table; +use Google\Cloud\BigQuery\ValueMapper; use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Storage\Connection\ConnectionInterface as StorageConnectionInterface; use Google\Cloud\Upload\MultipartUploader; @@ -39,6 +40,7 @@ class TableTest extends SnippetTestCase private $info; private $connection; private $table; + private $mapper; public function setUp() { @@ -61,12 +63,14 @@ public function setUp() 'friendlyName' => 'Jeffrey' ]; + $this->mapper = new ValueMapper(false); $this->connection = $this->prophesize(ConnectionInterface::class); $this->table = new \TableStub( $this->connection->reveal(), self::ID, self::DSID, self::PROJECT, + $this->mapper, $this->info ); } @@ -127,7 +131,7 @@ public function testRows() $res = $snippet->invoke('rows'); $this->assertInstanceOf(\Generator::class, $res->returnVal()); - $this->assertEquals('abcd', $res->output()); + $this->assertEquals('abcd' . PHP_EOL, $res->output()); } public function testCopy() diff --git a/tests/system/BigQuery/BigQueryTestCase.php b/tests/system/BigQuery/BigQueryTestCase.php index d8f04a8c4a61..ed9bc88d17c2 100644 --- a/tests/system/BigQuery/BigQueryTestCase.php +++ b/tests/system/BigQuery/BigQueryTestCase.php @@ -39,18 +39,7 @@ public static function setUpBeforeClass() } $keyFilePath = getenv('GOOGLE_CLOUD_PHP_TESTS_KEY_PATH'); - $schema = [ - 'fields' => [ - [ - 'name' => 'city', - 'type' => 'STRING' - ], - [ - 'name' => 'state', - 'type' => 'STRING' - ] - ] - ]; + $schema = json_decode(file_get_contents(__DIR__ . '/../data/table-schema.json'), true); self::$bucket = (new StorageClient([ 'keyFilePath' => $keyFilePath ]))->createBucket(uniqid(self::TESTING_PREFIX)); @@ -59,7 +48,9 @@ public static function setUpBeforeClass() ]); self::$dataset = self::$client->createDataset(uniqid(self::TESTING_PREFIX)); self::$table = self::$dataset->createTable(uniqid(self::TESTING_PREFIX), [ - 'schema' => $schema + 'schema' => [ + 'fields' => $schema + ] ]); self::$hasSetUp = true; } diff --git a/tests/system/BigQuery/LoadDataAndQueryTest.php b/tests/system/BigQuery/LoadDataAndQueryTest.php index 780b3569bb4b..99c917c675f4 100644 --- a/tests/system/BigQuery/LoadDataAndQueryTest.php +++ b/tests/system/BigQuery/LoadDataAndQueryTest.php @@ -17,7 +17,11 @@ namespace Google\Cloud\Tests\System\BigQuery; +use Google\Cloud\BigQuery\Bytes; use Google\Cloud\BigQuery\Date; +use Google\Cloud\BigQuery\Time; +use Google\Cloud\BigQuery\Timestamp; +use Google\Cloud\BigQuery\ValueMapper; use Google\Cloud\ExponentialBackoff; use GuzzleHttp\Psr7; @@ -27,131 +31,221 @@ class LoadDataAndQueryTest extends BigQueryTestCase { private static $expectedRows = 0; + private $row; + + public function setUp() + { + $this->row = [ + 'Name' => 'Dave', + 'Age' => 101, + 'Weight' => 100.5, + 'IsMagic' => true, + 'Spells' => [ + [ + 'Name' => 'Summon Dragon', + 'LastUsed' => self::$client->timestamp(new \DateTime('2000-01-01 23:59:56 UTC')), + 'DiscoveredBy' => 'Bobby', + 'Properties' => [ + [ + 'Name' => 'Fire', + 'Power' => 300.2 + ] + ], + 'Icon' => self::$client->bytes('icon') + ] + ], + 'ImportantDates' => [ + 'TeaTime' => self::$client->time(new \DateTime('15:15:12')), + 'NextVacation' => self::$client->date(new \DateTime('2020-10-11')), + 'FavoriteTime' => new \DateTime('1920-01-01 15:15:12') + ], + 'FavoriteNumbers' => [10, 11] + ]; + } + + public function testInsertRowToTable() + { + self::$expectedRows++; + $insertResponse = self::$table->insertRow($this->row); + sleep(1); + $rows = iterator_to_array(self::$table->rows()); + $actualRow = $rows[0]; + + $this->assertTrue($insertResponse->isSuccessful()); + $this->assertEquals(self::$expectedRows, count($rows)); + + $expectedRow = $this->row; + $expectedBytes = $expectedRow['Spells'][0]['Icon']; + $actualBytes = $actualRow['Spells'][0]['Icon']; + unset($expectedRow['Spells'][0]['Icon']); + unset($actualRow['Spells'][0]['Icon']); + + $this->assertEquals($expectedRow, $actualRow); + $this->assertEquals((string) $expectedBytes, (string) $actualBytes); + } /** - * @dataProvider rowProvider + * @depends testInsertRowToTable + * @dataProvider useLegacySqlProvider */ - public function testLoadsDataToTable($data) + public function testRunQuery($useLegacySql) { - $job = self::$table->load($data, [ - 'jobConfig' => [ - 'sourceFormat' => 'CSV' - ] + $query = sprintf( + $useLegacySql + ? 'SELECT Name, Age, Weight, IsMagic, Spells.* FROM [%s.%s]' + : 'SELECT Name, Age, Weight, IsMagic, Spells FROM `%s.%s`', + self::$dataset->id(), + self::$table->id() + ); + $results = self::$client->runQuery($query, [ + 'useLegacySql' => $useLegacySql ]); $backoff = new ExponentialBackoff(8); - $backoff->execute(function () use ($job) { - $job->reload(); + $backoff->execute(function () use ($results) { + $results->reload(); - if (!$job->isComplete()) { + if (!$results->isComplete()) { throw new \Exception(); } }); - if (!$job->isComplete()) { - $this->fail('Job failed to complete within the allotted time.'); + if (!$results->isComplete()) { + $this->fail('Query did not complete within the allotted time.'); } - self::$expectedRows += count(file(__DIR__ . '/../data/table-data.csv')); - $actualRows = count(iterator_to_array(self::$table->rows())); - - $this->assertEquals(self::$expectedRows, $actualRows); - } - - public function rowProvider() - { - $data = file_get_contents(__DIR__ . '/../data/table-data.csv'); - - return [ - [$data], - [fopen(__DIR__ . '/../data/table-data.csv', 'r')], - [Psr7\stream_for($data)] - ]; + $actualRow = iterator_to_array($results->rows())[0]; + + if ($useLegacySql) { + $spells = $this->row['Spells'][0]; + + $this->assertEquals($this->row['Name'], $actualRow['Name']); + $this->assertEquals($this->row['Age'], $actualRow['Age']); + $this->assertEquals($this->row['Weight'], $actualRow['Weight']); + $this->assertEquals($this->row['IsMagic'], $actualRow['IsMagic']); + $this->assertEquals($spells['Name'], $actualRow['Spells_Name']); + $this->assertEquals($spells['LastUsed'], $actualRow['Spells_LastUsed']); + $this->assertEquals($spells['DiscoveredBy'], $actualRow['Spells_DiscoveredBy']); + $this->assertEquals($spells['Properties'][0]['Name'], $actualRow['Spells_Properties_Name']); + $this->assertEquals($spells['Properties'][0]['Power'], $actualRow['Spells_Properties_Power']); + $this->assertEquals((string) $spells['Icon'], (string) $actualRow['Spells_Icon']); + } else { + $expectedRow = $this->row; + $expectedBytes = $expectedRow['Spells'][0]['Icon']; + $actualBytes = $actualRow['Spells'][0]['Icon']; + unset($expectedRow['ImportantDates']); + unset($expectedRow['FavoriteNumbers']); + unset($expectedRow['Spells'][0]['Icon']); + unset($actualRow['Spells'][0]['Icon']); + + $this->assertEquals($expectedRow, $actualRow); + $this->assertEquals((string) $expectedBytes, (string) $actualBytes); + } } /** - * @depends testLoadsDataToTable + * @depends testInsertRowToTable + * @dataProvider useLegacySqlProvider */ - public function testLoadsDataFromStorageToTable() + public function testRunQueryAsJob($useLegacySql) { - $object = self::$bucket->upload( - fopen(__DIR__ . '/../data/table-data.csv', 'r') + $query = sprintf( + $useLegacySql + ? 'SELECT FavoriteNumbers, ImportantDates.* FROM [%s.%s]' + : 'SELECT FavoriteNumbers, ImportantDates FROM `%s.%s`', + self::$dataset->id(), + self::$table->id() ); - self::$deletionQueue[] = $object; - - $job = self::$table->loadFromStorage($object, [ + $job = self::$client->runQueryAsJob($query, [ 'jobConfig' => [ - 'sourceFormat' => 'CSV' + 'useLegacySql' => $useLegacySql ] ]); + $results = $job->queryResults(); $backoff = new ExponentialBackoff(8); - $backoff->execute(function () use ($job) { - $job->reload(); + $backoff->execute(function () use ($results) { + $results->reload(); - if (!$job->isComplete()) { + if (!$results->isComplete()) { throw new \Exception(); } }); - if (!$job->isComplete()) { - $this->fail('Job failed to complete within the allotted time.'); + + if (!$results->isComplete()) { + $this->fail('Query did not complete within the allotted time.'); } - self::$expectedRows += count(file(__DIR__ . '/../data/table-data.csv')); - $actualRows = count(iterator_to_array(self::$table->rows())); + $actualRows = iterator_to_array($results->rows()); - $this->assertEquals(self::$expectedRows, $actualRows); + if ($useLegacySql) { + $dates = $this->row['ImportantDates']; + $numbers = $this->row['FavoriteNumbers']; + + $this->assertEquals($numbers[0], $actualRows[0]['FavoriteNumbers']); + $this->assertEquals($numbers[1], $actualRows[1]['FavoriteNumbers']); + $this->assertEquals($dates['TeaTime'], $actualRows[0]['ImportantDates_TeaTime']); + $this->assertEquals($dates['NextVacation'], $actualRows[0]['ImportantDates_NextVacation']); + $this->assertEquals($dates['FavoriteTime'], $actualRows[0]['ImportantDates_FavoriteTime']); + } else { + $expectedRow = [ + 'FavoriteNumbers' => $this->row['FavoriteNumbers'], + 'ImportantDates' => $this->row['ImportantDates'] + ]; + + $this->assertEquals($expectedRow, $actualRows[0]); + } } - /** - * @depends testLoadsDataFromStorageToTable - */ - public function testInsertRowToTable() + public function useLegacySqlProvider() { - self::$expectedRows++; - $insertResponse = self::$table->insertRow([ - 'city' => 'Detroit', - 'state' => 'MI' - ]); - - $this->assertTrue($insertResponse->isSuccessful()); + return [ + [true], + [false] + ]; } - /** - * @depends testInsertRowToTable - */ - public function testInsertRowsToTable() + public function testRunQueryWithNamedParameters() { - $rows = [ - [ - 'data' => [ - 'city' => 'Detroit', - 'state' => 'MI' + $query = 'SELECT' + . '@structType as structType,' + . '@arrayStruct as arrayStruct,' + . '@nestedStruct as nestedStruct,' + . '@arrayType as arrayType,' + . '@name as name,' + . '@int as int,' + . '@float as float,' + . '@timestamp as timestamp,' + . '@datetime as datetime,' + . '@date as date,' + . '@time as time,' + . '@bytes as bytes'; + + $bytes = self::$client->bytes('123'); + $params = [ + 'structType' => [ + 'hello' => 'world' + ], + 'arrayStruct' => [ + [ + 'hello' => 'world' ] ], - [ - 'data' => [ - 'city' => 'Ann Arbor', - 'state' => 'MI' + 'nestedStruct' => [ + 'hello' => [ + 'wor' => 'ld' ] - ] + ], + 'arrayType' => [1,2,3], + 'name' => 'Dave', + 'int' => 5, + 'float' => 5.5, + 'timestamp' => self::$client->timestamp(new \DateTime('2003-02-05 11:15:02.421827Z')), + 'datetime' => new \DateTime('2003-02-05 11:15:02.421827Z'), + 'date' => self::$client->date(new \DateTime('2003-12-12')), + 'time' => self::$client->time(new \DateTime('11:15:02')), + 'bytes' => $bytes ]; - self::$expectedRows += count($rows); - $insertResponse = self::$table->insertRows($rows); + $results = self::$client->runQuery($query, ['parameters' => $params]); - $this->assertTrue($insertResponse->isSuccessful()); - } - - /** - * @depends testInsertRowsToTable - */ - public function testRunQuery() - { - $results = self::$client->runQuery( - sprintf( - 'SELECT * FROM [%s.%s]', - self::$dataset->id(), - self::$table->id() - ) - ); $backoff = new ExponentialBackoff(8); $backoff->execute(function () use ($results) { $results->reload(); @@ -165,30 +259,20 @@ public function testRunQuery() $this->fail('Query did not complete within the allotted time.'); } - $actualRows = count(iterator_to_array($results->rows())); + $actualRow = iterator_to_array($results->rows())[0]; + $actualBytes = $actualRow['bytes']; + unset($params['bytes']); + unset($actualRow['bytes']); - $this->assertEquals(self::$expectedRows, $actualRows); + $this->assertEquals($params, $actualRow); + $this->assertEquals((string) $bytes, (string) $actualBytes); } - public function testRunQueryWithNamedParameters() + public function testRunQueryWithPositionalParameters() { - $date = '2000-01-01'; - $query = 'WITH data AS' - . '(SELECT "Dave" as name, DATE("1999-01-01") as date, 1.1 as floatNum, 1 as intNum, false as boolVal ' - . 'UNION ALL ' - . 'SELECT "John" as name, DATE("2000-01-01") as date, 1.2 as floatNum, 2 as intNum, true as boolVal) ' - . 'SELECT * FROM data ' - . 'WHERE name = @name AND date >= @date AND floatNum = @numbers.floatNum AND intNum = @numbers.intNum AND boolVal = @boolVal'; - - $results = self::$client->runQuery($query, [ + $results = self::$client->runQuery('SELECT 1 IN UNNEST(?) AS arr', [ 'parameters' => [ - 'name' => 'John', - 'date' => self::$client->date(new \DateTime($date)), - 'numbers' => [ - 'floatNum' => 1.2, - 'intNum' => 2, - ], - 'boolVal' => true + [1, 2, 3] ] ]); $backoff = new ExponentialBackoff(8); @@ -206,25 +290,21 @@ public function testRunQueryWithNamedParameters() $actualRows = iterator_to_array($results->rows()); $expectedRows = [ - [ - 'name' => 'John', - 'floatNum' => 1.2, - 'intNum' => 2, - 'boolVal' => true, - 'date' => new Date(new \DateTime($date)) - ] + ['arr' => true] ]; $this->assertEquals($expectedRows, $actualRows); } - public function testRunQueryWithPositionalParameters() + public function testRunQueryAsJobWithNamedParameters() { - $results = self::$client->runQuery('SELECT 1 IN UNNEST(?) AS arr', [ + $query = 'SELECT @int as int'; + $job = self::$client->runQueryAsJob($query, [ 'parameters' => [ - [1, 2, 3] + 'int' => 5 ] ]); + $results = $job->queryResults(); $backoff = new ExponentialBackoff(8); $backoff->execute(function () use ($results) { $results->reload(); @@ -239,25 +319,18 @@ public function testRunQueryWithPositionalParameters() } $actualRows = iterator_to_array($results->rows()); - $expectedRows = [ - ['arr' => true] - ]; + $expectedRows = [['int' => 5]]; $this->assertEquals($expectedRows, $actualRows); } - /** - * @depends testInsertRowsToTable - */ - public function testRunQueryAsJob() + public function testRunQueryAsJobWithPositionalParameters() { - $job = self::$client->runQueryAsJob( - sprintf( - 'SELECT * FROM [%s.%s]', - self::$dataset->id(), - self::$table->id() - ) - ); + $job = self::$client->runQueryAsJob('SELECT 1 IN UNNEST(?) AS arr', [ + 'parameters' => [ + [1, 2, 3] + ] + ]); $results = $job->queryResults(); $backoff = new ExponentialBackoff(8); $backoff->execute(function () use ($results) { @@ -272,86 +345,102 @@ public function testRunQueryAsJob() $this->fail('Query did not complete within the allotted time.'); } - $actualRows = count(iterator_to_array($results->rows())); + $actualRows = iterator_to_array($results->rows()); + $expectedRows = [ + ['arr' => true] + ]; - $this->assertEquals(self::$expectedRows, $actualRows); + $this->assertEquals($expectedRows, $actualRows); } - public function testRunQueryAsJobWithNamedParameters() + /** + * @dataProvider rowProvider + * @depends testInsertRowToTable + */ + public function testLoadsDataToTable($data) { - $date = '2000-01-01'; - $query = 'WITH data AS' - . '(SELECT "Dave" as name, DATE("1999-01-01") as date, 1.1 as floatNum, 1 as intNum, true as boolVal ' - . 'UNION ALL ' - . 'SELECT "John" as name, DATE("2000-01-01") as date, 1.2 as floatNum, 2 as intNum, false as boolVal) ' - . 'SELECT * FROM data ' - . 'WHERE name = @name AND date >= @date AND floatNum = @numbers.floatNum AND intNum = @numbers.intNum AND boolVal = @boolVal'; - - $job = self::$client->runQueryAsJob($query, [ - 'parameters' => [ - 'name' => 'John', - 'date' => self::$client->date(new \DateTime($date)), - 'numbers' => [ - 'floatNum' => 1.2, - 'intNum' => 2, - ], - 'boolVal' => false + $job = self::$table->load($data, [ + 'jobConfig' => [ + 'sourceFormat' => 'NEWLINE_DELIMITED_JSON' ] ]); - $results = $job->queryResults(); $backoff = new ExponentialBackoff(8); - $backoff->execute(function () use ($results) { - $results->reload(); + $backoff->execute(function () use ($job) { + $job->reload(); - if (!$results->isComplete()) { + if (!$job->isComplete()) { throw new \Exception(); } }); - if (!$results->isComplete()) { - $this->fail('Query did not complete within the allotted time.'); + if (!$job->isComplete()) { + $this->fail('Job failed to complete within the allotted time.'); } - $actualRows = iterator_to_array($results->rows()); - $expectedRows = [ - [ - 'name' => 'John', - 'floatNum' => 1.2, - 'intNum' => 2, - 'boolVal' => false, - 'date' => new Date(new \DateTime($date)) - ] - ]; + self::$expectedRows += count(file(__DIR__ . '/../data/table-data.json')); + $actualRows = count(iterator_to_array(self::$table->rows())); - $this->assertEquals($expectedRows, $actualRows); + $this->assertEquals(self::$expectedRows, $actualRows); } - public function testRunQueryAsJobWithPositionalParameters() + public function rowProvider() { - $job = self::$client->runQueryAsJob('SELECT 1 IN UNNEST(?) AS arr', [ - 'parameters' => [ - [1, 2, 3] + $data = file_get_contents(__DIR__ . '/../data/table-data.json'); + + return [ + [$data], + [fopen(__DIR__ . '/../data/table-data.json', 'r')], + [Psr7\stream_for($data)] + ]; + } + + /** + * @depends testLoadsDataToTable + */ + public function testLoadsDataFromStorageToTable() + { + $object = self::$bucket->upload( + fopen(__DIR__ . '/../data/table-data.json', 'r') + ); + self::$deletionQueue[] = $object; + + $job = self::$table->loadFromStorage($object, [ + 'jobConfig' => [ + 'sourceFormat' => 'NEWLINE_DELIMITED_JSON' ] ]); - $results = $job->queryResults(); $backoff = new ExponentialBackoff(8); - $backoff->execute(function () use ($results) { - $results->reload(); + $backoff->execute(function () use ($job) { + $job->reload(); - if (!$results->isComplete()) { + if (!$job->isComplete()) { throw new \Exception(); } }); - - if (!$results->isComplete()) { - $this->fail('Query did not complete within the allotted time.'); + if (!$job->isComplete()) { + $this->fail('Job failed to complete within the allotted time.'); } - $actualRows = iterator_to_array($results->rows()); - $expectedRows = [ - ['arr' => true] + self::$expectedRows += count(file(__DIR__ . '/../data/table-data.json')); + $actualRows = count(iterator_to_array(self::$table->rows())); + + $this->assertEquals(self::$expectedRows, $actualRows); + } + + /** + * @depends testLoadsDataToTable + */ + public function testInsertRowsToTable() + { + $rows = [ + ['data' => $this->row], + ['data' => $this->row] ]; + self::$expectedRows += count($rows); + $insertResponse = self::$table->insertRows($rows); + $actualRows = count(iterator_to_array(self::$table->rows())); - $this->assertEquals($expectedRows, $actualRows); + $this->assertTrue($insertResponse->isSuccessful()); + $this->assertEquals(self::$expectedRows, $actualRows); } } diff --git a/tests/system/data/table-data.csv b/tests/system/data/table-data.csv deleted file mode 100644 index 687d1cd433bc..000000000000 --- a/tests/system/data/table-data.csv +++ /dev/null @@ -1,2 +0,0 @@ -Detroit,MI -Ann Arbor,MI diff --git a/tests/system/data/table-data.json b/tests/system/data/table-data.json new file mode 100644 index 000000000000..b8321597a98b --- /dev/null +++ b/tests/system/data/table-data.json @@ -0,0 +1,3 @@ +{"Name":"Bilbo","Age":"111","Weight":67.2,"IsMagic":false,"Spells":[],"ImportantDates":{"TeaTime":"10:00:00","NextVacation":"2017-09-22","FavoriteTime":"2031-04-01T05:09:27"},"FavoriteNumbers":[1]} +{"Name":"Gandalf","Age":"1000","Weight":198.6,"IsMagic":true,"Spells":[{"Name": "Skydragon", "Icon":"iVBORw0KGgoAAAANSUhEUgAAAB4AAAAgCAYAAAAFQMh/AAAAAXNSR0IArs4c6QAAA9lJREFUSA21lk9OVEEQxvsRDImoiMG9mLjjCG5mEg7gEfQGsIcF7p0EDsBBSJiNO7ZsFRZqosb/QkSj7fer7ur33sw8GDFUUq+7q6vqq6qu7pkQzqG4EeI521e7FePVgM9cGPYwhCi6UO8qFOK+YY+Br66ujsmmxb84Yzwp6zCsxjJfWVkxnMsEMGuWHZ9Wcz11cM48hkq0vLwc1tbW4mAwqDpcdIqnMmgF0JMv2CiGnZ2dcHR0FA4PD8Pe3t5U/tx6bCSlb+JT8XfxT3HsUek0Li0tRdjWl+z6iRF+FNA1hXPDQ/IMNyRg3s8bD/OaZS+VP+9cOLSa64cA34oXZWagDkRzAaJxXaE+ufc4rCN7LrazZ2+8+STtpAL8WYDvpTaHKlkB2iQARMvb2+H27m4YaL7zaDtUw1BZAASi6T8T2UZnPZV2pvnJfCH5p8bewcGB6TrIfz8wBZgHQ83kjpuj6RBYQpuo09Tvmpd7TPe+ktZN8cKwS92KWXGuaqWowlYEwthtMcWOZUNJc8at+zuF/Xkqo69baS7P+AvWjYwJ4jyHXXsEnd74ZO/Pq+uXUuv6WNlso6cvnDsZB1V/unJab3D1/KrJDw9NCM9wHf2FK2ejTKMejnBHfGtfH7LGGCdQDqaqJgfgzWjXK1nYV4jRbPGnxUT7cqUaZfJrVZeOm9QmB21L6xXgbu/ScsYusJFMoU0x2fsamRJOd6kOYDRLUxv94ENZe8+0gM+0dyz+KgU7X8rLHHCIOZyrna4y6ykIu0YCs02TBXmk3PZssmEgaTxTo83xjCIjoE21h0Yah3MrV4+9kR8MaabGze+9NEILGAFE5nMOiiA32KnAr/sb7tED3nzlzC4dB38WMC+EjaqHfqvUKHi2gJPdWQ6AbH8hgyQ7QY6jvjj3QZWvX6pUAtduTX5Dss96Q7NI9RQRJeeKvRFbt0v2gb1Gx/PooJsztn1c1DqpAU3Hde2dB2aEHBhjgOFjMeDvxLafjQ3YZQSgOcHJZX611H45sGLHWvYTz9hiURlpNoBZvxb/Ft9lAQ1DmBfUiR+j1hAPkMBTE9L9+zLva1QvGFHurRBaZ5xLVitoBviiRkD/sIMDztKA5FA0b9/0OclzO2/XAQymJ0TcghZwEo9/AX8gMeAJMOvIsWWt5bwCoiFhVSllrdH0t5Q1JHAFlKJNkvTVdn2GHb9KdmacMT+d/Os05imJUccRX2YuZ93Sxf0Ilc4DPDeAq5SAvFEAY94cQc6BA26dzb4HWAJI4DPmQE5KCVUyvb2FcDZem7JdT2ggKUP3xX6n9XNq1DpzSf4Cy4ZqSlmM8d8AAAAASUVORK5CYII=","DiscoveredBy":"Firebreather","Properties":[{"Name":"Flying","Power":1}],"LastUsed":"2015-10-31 23:59:56 UTC"}],"ImportantDates":{"TeaTime":"15:00:00","NextVacation":"2666-06-06","FavoriteTime":"2001-12-19T23:59:59"},"FavoriteNumbers":[3]} +{"Name":"Sabrina","Age":"17","Weight":128.3,"IsMagic":true,"Spells":[{"Name": "Talking cats", "Icon":"iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAABxpRE9UAAAAAgAAAAAAAAAgAAAAKAAAACAAAAAgAAABxj2CfowAAAGSSURBVHgB7Jc9TsNAEIX3JDkCPUV6KlpKFHEGlD4nyA04ACUXQKTgCEipUnKGNEbP0otentayicZ24SlWs7tjO/N9u/5J2b2+NUtuZcnwYE8BuQPyGZAPwXwLLPk5kG+BJa9+fgfkh1B+CeancL4F8i2Q/wWm/S/w+XFoTseftn0dvhu0OXfhpM+AGvzcEiYVAFisPqE9zrETJhHAlXfg2lglMK9z0f3RBfB+ZyRUV3x+erzsEIjjOBqc1xtNAIrvguybV3A9lkVHxlEE6GrrPb/ZvAySwlUnfCmlPQ+R8JCExvGtcRQBLFwj4FGkznX1VYDKPG/f2/MjwCksXACgdNUxJjwK9xwl4JihOwTFR0kIF+CABEPRnvsvPFctMoYKqAFSAFaMwB4pp3Y+bodIYL9WmIAaIOHxo7W8wiHvAjTvhUeNwwSgeAeAABbqOewC5hBdwFD4+9+7puzXV9fS6/b1wwT4tsaYAhwOOQdUQch5vgZCeAhAv3ZM31yYAAUgvApQQQ6n5w6FB/RVe1jdJOAPAAD//1eMQwoAAAGQSURBVO1UMU4DQQy8X9AgWopIUINEkS4VlJQo4gvwAV7AD3gEH4iSgidESpWSXyyZExP5lr0c7K5PsXBhec/2+jzjuWtent9CLdtu1mG5+gjz+WNr7IsY7eH+tvO+xfuqk4vz7CH91edFaF5v9nb6dBKm13edvrL+0Lk5lMzJkQDeJSkkgHF6mR8CHwMHCQR/NAQQGD0BAlwK4FCefQiefq+A2Vn29tG7igLAfmwcnJu/nJy3BMQkMN9HEPr8AL3bfBv7Bp+7/SoExMDjZwKEJwmyhnnmQIQEBIlz2x0iKoAvJkAC6TsTIH6MqRrEWUMSZF2zAwqT4Eu/e6pzFAIkmNSZ4OFT+VYBIIF//UqbJwnF/4DU0GwOn8r/JQYCpPGufEfJuZiA37ycQw/5uFeqPq4pfR6FADmkBCXjfWdZj3NfXW58dAJyB9W65wRoMWulryvAyqa05nQFaDFrpa8rwMqmtOZ0BWgxa6WvK8DKprTmdAVoMWulryvAyqa05nQFaDFrpa8rwMqmtOb89wr4AtQ4aPoL6yVpAAAAAElFTkSuQmCC","DiscoveredBy":"Salem","Properties":[{"Name":"Makes you look crazy","Power":1}],"LastUsed":"2017-02-14 12:07:23 UTC"}],"ImportantDates":{"TeaTime":"12:00:00","NextVacation":"2017-03-14","FavoriteTime":"2000-10-31T23:27:46"},"FavoriteNumbers":[7]} diff --git a/tests/system/data/table-schema.json b/tests/system/data/table-schema.json new file mode 100644 index 000000000000..9a676d1a19ae --- /dev/null +++ b/tests/system/data/table-schema.json @@ -0,0 +1,93 @@ +[ + { + "mode": "NULLABLE", + "name": "Name", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "Age", + "type": "INTEGER" + }, + { + "mode": "NULLABLE", + "name": "Weight", + "type": "FLOAT" + }, + { + "mode": "NULLABLE", + "name": "IsMagic", + "type": "BOOLEAN" + }, + { + "fields": [ + { + "mode": "NULLABLE", + "name": "Name", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "LastUsed", + "type": "TIMESTAMP" + }, + { + "mode": "NULLABLE", + "name": "DiscoveredBy", + "type": "STRING" + }, + { + "fields": [ + { + "mode": "NULLABLE", + "name": "Name", + "type": "STRING" + }, + { + "mode": "NULLABLE", + "name": "Power", + "type": "FLOAT" + } + ], + "mode": "REPEATED", + "name": "Properties", + "type": "RECORD" + }, + { + "mode": "NULLABLE", + "name": "Icon", + "type": "BYTES" + } + ], + "mode": "REPEATED", + "name": "Spells", + "type": "RECORD" + }, + { + "fields": [ + { + "mode": "NULLABLE", + "name": "TeaTime", + "type": "TIME" + }, + { + "mode": "NULLABLE", + "name": "NextVacation", + "type": "DATE" + }, + { + "mode": "NULLABLE", + "name": "FavoriteTime", + "type": "DATETIME" + } + ], + "mode": "NULLABLE", + "name": "ImportantDates", + "type": "RECORD" + }, + { + "mode": "REPEATED", + "name": "FavoriteNumbers", + "type": "INTEGER" + } +] diff --git a/tests/unit/BigQuery/DatasetTest.php b/tests/unit/BigQuery/DatasetTest.php index ead3bcf574e4..10b1f1da2140 100644 --- a/tests/unit/BigQuery/DatasetTest.php +++ b/tests/unit/BigQuery/DatasetTest.php @@ -20,6 +20,7 @@ use Google\Cloud\BigQuery\Connection\ConnectionInterface; use Google\Cloud\BigQuery\Dataset; use Google\Cloud\BigQuery\Table; +use Google\Cloud\BigQuery\ValueMapper; use Google\Cloud\Exception\NotFoundException; use Prophecy\Argument; @@ -29,18 +30,26 @@ class DatasetTest extends \PHPUnit_Framework_TestCase { public $connection; + public $mapper; public $projectId = 'myProjectId'; public $datasetId = 'myDatasetId'; public $tableId = 'myTableId'; public function setUp() { + $this->mapper = new ValueMapper(false); $this->connection = $this->prophesize(ConnectionInterface::class); } public function getDataset($connection, array $data = []) { - return new Dataset($connection->reveal(), $this->datasetId, $this->projectId, $data); + return new Dataset( + $connection->reveal(), + $this->datasetId, + $this->projectId, + $this->mapper, + $data + ); } public function testDoesExistTrue() diff --git a/tests/unit/BigQuery/TableTest.php b/tests/unit/BigQuery/TableTest.php index 3051dd0c2d9b..b20fd86338c0 100644 --- a/tests/unit/BigQuery/TableTest.php +++ b/tests/unit/BigQuery/TableTest.php @@ -21,6 +21,7 @@ use Google\Cloud\BigQuery\InsertResponse; use Google\Cloud\BigQuery\Job; use Google\Cloud\BigQuery\Table; +use Google\Cloud\BigQuery\ValueMapper; use Google\Cloud\Exception\NotFoundException; use Google\Cloud\Storage\Connection\ConnectionInterface as StorageConnectionInterface; use Google\Cloud\Storage\StorageObject; @@ -34,6 +35,7 @@ class TableTest extends \PHPUnit_Framework_TestCase { public $connection; public $storageConnection; + public $mapper; public $fileName = 'myfile.csv'; public $bucketName = 'myBucket'; public $projectId = 'myProjectId'; @@ -46,7 +48,12 @@ class TableTest extends \PHPUnit_Framework_TestCase ]; public $schemaData = [ 'schema' => [ - 'fields' => [['name' => 'first_name']] + 'fields' => [ + [ + 'name' => 'first_name', + 'type' => 'STRING' + ] + ] ] ]; public $insertJobResponse = [ @@ -57,6 +64,7 @@ class TableTest extends \PHPUnit_Framework_TestCase public function setUp() { + $this->mapper = new ValueMapper(false); $this->connection = $this->prophesize(ConnectionInterface::class); $this->storageConnection = $this->prophesize(StorageConnectionInterface::class); } @@ -77,6 +85,7 @@ public function getTable($connection, array $data = [], $tableId = null) $tableId ?: $this->tableId, $this->datasetId, $this->projectId, + $this->mapper, $data ); } diff --git a/tests/unit/BigQuery/ValueMapperTest.php b/tests/unit/BigQuery/ValueMapperTest.php index 1d8374803f44..58a57ccc2556 100644 --- a/tests/unit/BigQuery/ValueMapperTest.php +++ b/tests/unit/BigQuery/ValueMapperTest.php @@ -201,6 +201,34 @@ public function testMapsBytesFromBigQuery() $this->assertEquals((string) new Bytes('abcd'), (string) $actual); } + /** + * @dataProvider toBigQueryValueProvider + */ + public function testMapsToBigQuery($value, $expected) + { + $mapper = new ValueMapper(false); + $actual = $mapper->toBigQuery($value); + + $this->assertEquals($expected, $actual); + } + + public function toBigQueryValueProvider() + { + $dt = new \DateTime(); + $date = new Date($dt); + $int64 = new Int64('123'); + + return [ + [$dt, $dt->format('Y-m-d\TH:i:s.u')], + [$date, (string) $date], + [ + ['date' => $date], + ['date' => (string) $date] + ], + [1, 1] + ]; + } + /** * @dataProvider parameterValueProvider */ From c0da5beea98991345d3aeb86be847bec8ecfaba6 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Mon, 27 Feb 2017 11:38:19 -0500 Subject: [PATCH 070/107] remove rate limit retries (#356) --- src/RequestWrapper.php | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/src/RequestWrapper.php b/src/RequestWrapper.php index 66d3e6579aae..3e76cf9b9f2f 100644 --- a/src/RequestWrapper.php +++ b/src/RequestWrapper.php @@ -64,14 +64,6 @@ class RequestWrapper 503 ]; - /** - * @var array - */ - private $httpRetryMessages = [ - 'rateLimitExceeded', - 'userRateLimitExceeded' - ]; - /** * @var bool $shouldSignRequest Whether to enable request signing. */ @@ -246,27 +238,14 @@ private function getExceptionMessage(\Exception $ex) private function getRetryFunction() { $httpRetryCodes = $this->httpRetryCodes; - $httpRetryMessages = $this->httpRetryMessages; - return function (\Exception $ex) use ($httpRetryCodes, $httpRetryMessages) { + return function (\Exception $ex) use ($httpRetryCodes) { $statusCode = $ex->getCode(); if (in_array($statusCode, $httpRetryCodes)) { return true; } - $message = json_decode($ex->getMessage(), true); - - if (!isset($message['error']['errors'])) { - return false; - } - - foreach ($message['error']['errors'] as $error) { - if (in_array($error['reason'], $httpRetryMessages)) { - return true; - } - } - return false; }; } From fd5d476d4f26184bf28b9c4de14f8d07547b8be2 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Mon, 27 Feb 2017 12:17:01 -0500 Subject: [PATCH 071/107] Add snippet tests for Compute (#353) --- src/Compute/Metadata.php | 24 +-- .../Metadata/Readers/ReaderInterface.php | 29 ++++ src/Compute/Metadata/Readers/StreamReader.php | 2 +- tests/snippets/Compute/MetadataTest.php | 138 ++++++++++++++++++ 4 files changed, 183 insertions(+), 10 deletions(-) create mode 100644 src/Compute/Metadata/Readers/ReaderInterface.php create mode 100644 tests/snippets/Compute/MetadataTest.php diff --git a/src/Compute/Metadata.php b/src/Compute/Metadata.php index 20118828d751..b7424849de94 100644 --- a/src/Compute/Metadata.php +++ b/src/Compute/Metadata.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Compute; use Google\Cloud\Compute\Metadata\Readers\StreamReader; +use Google\Cloud\Compute\Metadata\Readers\ReaderInterface; /** * A library for accessing the Google Compute Engine (GCE) metadata. @@ -27,12 +28,17 @@ * * You can get the GCE metadata values very easily like: * + * + * Example: * ``` * use Google\Cloud\Compute\Metadata; * * $metadata = new Metadata(); - * $project_id = $metadata->getProjectId(); + * $projectId = $metadata->getProjectId(); + * ``` * + * ``` + * // It is easy to get any metadata from a project. * $val = $metadata->getProjectMetadata($key); * ``` */ @@ -59,9 +65,9 @@ public function __construct() /** * Replace the default reader implementation * - * @param mixed $reader The reader implementation + * @param ReaderInterface $reader The reader implementation */ - public function setReader($reader) + public function setReader(ReaderInterface $reader) { $this->reader = $reader; } @@ -71,7 +77,7 @@ public function setReader($reader) * * Example: * ``` - * $projectId = $reader->get('project/project-id'); + * $projectId = $metadata->get('project/project-id'); * ``` * * @param string $path The path of the item to retrieve. @@ -86,15 +92,15 @@ public function get($path) * * Example: * ``` - * $projectId = $reader->getProjectId(); + * $projectId = $metadata->getProjectId(); * ``` * * @return string */ public function getProjectId() { - if (! isset($this->projectId)) { - $this->projectId = $this->reader->read('project/project-id'); + if (!isset($this->projectId)) { + $this->projectId = $this->get('project/project-id'); } return $this->projectId; @@ -105,7 +111,7 @@ public function getProjectId() * * Example: * ``` - * $foo = $reader->getProjectMetadata('foo'); + * $foo = $metadata->getProjectMetadata('foo'); * ``` * * @param string $key The metadata key @@ -122,7 +128,7 @@ public function getProjectMetadata($key) * * Example: * ``` - * $foo = $reader->getInstanceMetadata('foo'); + * $foo = $metadata->getInstanceMetadata('foo'); * ``` * * @param string $key The instance metadata key diff --git a/src/Compute/Metadata/Readers/ReaderInterface.php b/src/Compute/Metadata/Readers/ReaderInterface.php new file mode 100644 index 000000000000..1beb7ee9ff43 --- /dev/null +++ b/src/Compute/Metadata/Readers/ReaderInterface.php @@ -0,0 +1,29 @@ +reader = $this->prophesize(ReaderInterface::class); + $this->metadata = new Metadata; + $this->metadata->setReader($this->reader->reveal()); + } + + public function testClass() + { + $this->reader->read('project/project-id') + ->shouldBeCalled() + ->willReturn(self::PROJECT); + + $snippet = $this->snippetFromClass(Metadata::class); + $snippet->insertAfterLine(2, '$metadata->setReader($reader);'); + $snippet->addLocal('reader', $this->reader->reveal()); + $res = $snippet->invoke('projectId'); + + $this->assertEquals($res->returnVal(), self::PROJECT); + } + + public function testClassMetadata() + { + $key = 'foo'; + $val = 'bar'; + + $this->reader->read('project/attributes/'. $key) + ->shouldBeCalled() + ->willReturn($val); + + $this->metadata->setReader($this->reader->reveal()); + + $snippet = $this->snippetFromClass(Metadata::class, 1); + $snippet->addLocal('metadata', $this->metadata); + $snippet->addLocal('key', $key); + + $res = $snippet->invoke('val'); + $this->assertEquals($res->returnVal(), $val); + } + + public function testGet() + { + $this->reader->read('project/project-id') + ->shouldBeCalled() + ->willReturn(self::PROJECT); + + $this->metadata->setReader($this->reader->reveal()); + + $snippet = $this->snippetFromMethod(Metadata::class, 'get'); + $snippet->addLocal('metadata', $this->metadata); + $res = $snippet->invoke('projectId'); + + $this->assertEquals(self::PROJECT, $res->returnVal()); + } + + public function testGetProjectId() + { + $this->reader->read('project/project-id') + ->shouldBeCalled() + ->willReturn(self::PROJECT); + + $this->metadata->setReader($this->reader->reveal()); + + $snippet = $this->snippetFromMethod(Metadata::class, 'getProjectId'); + $snippet->addLocal('metadata', $this->metadata); + $res = $snippet->invoke('projectId'); + + $this->assertEquals(self::PROJECT, $res->returnVal()); + } + + public function testGetProjectMetadata() + { + $val = 'hello world'; + + $this->reader->read('project/attributes/foo') + ->shouldBeCalled() + ->willReturn($val); + + $this->metadata->setReader($this->reader->reveal()); + + $snippet = $this->snippetFromMethod(Metadata::class, 'getProjectMetadata'); + $snippet->addLocal('metadata', $this->metadata); + $res = $snippet->invoke('foo'); + + $this->assertEquals($val, $res->returnVal()); + } + + public function testGetInstanceMetadata() + { + $val = 'hello world'; + + $this->reader->read('instance/attributes/foo') + ->shouldBeCalled() + ->willReturn($val); + + $this->metadata->setReader($this->reader->reveal()); + + $snippet = $this->snippetFromMethod(Metadata::class, 'getInstanceMetadata'); + $snippet->addLocal('metadata', $this->metadata); + $res = $snippet->invoke('foo'); + + $this->assertEquals($val, $res->returnVal()); + } +} From effa6cb1787ce3812e03d5de35b30ffe274ef900 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Mon, 27 Feb 2017 12:17:14 -0500 Subject: [PATCH 072/107] Change ResourceNameTrait methods visibility (#358) --- src/PubSub/ResourceNameTrait.php | 6 +-- tests/unit/PubSub/ResourceNameTraitTest.php | 60 ++++++++++++--------- 2 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/PubSub/ResourceNameTrait.php b/src/PubSub/ResourceNameTrait.php index 7ad697f69a5b..9a2496d4852d 100644 --- a/src/PubSub/ResourceNameTrait.php +++ b/src/PubSub/ResourceNameTrait.php @@ -55,7 +55,7 @@ trait ResourceNameTrait * @return string * @throws \InvalidArgumentException */ - public function pluckName($type, $name) + private function pluckName($type, $name) { if (!isset($this->regexes[$type])) { throw new InvalidArgumentException(sprintf( @@ -85,7 +85,7 @@ public function pluckName($type, $name) * @return string * @throws \InvalidArgumentException */ - public function formatName($type, $name, $projectId = null) + private function formatName($type, $name, $projectId = null) { if (!isset($this->templates[$type])) { throw new InvalidArgumentException(sprintf( @@ -113,7 +113,7 @@ public function formatName($type, $name, $projectId = null) * @return bool * @throws \InvalidArgumentException */ - public function isFullyQualifiedName($type, $name) + private function isFullyQualifiedName($type, $name) { if (!isset($this->regexes[$type])) { throw new InvalidArgumentException(sprintf( diff --git a/tests/unit/PubSub/ResourceNameTraitTest.php b/tests/unit/PubSub/ResourceNameTraitTest.php index 2cd87498b60c..6cebb9d152cc 100644 --- a/tests/unit/PubSub/ResourceNameTraitTest.php +++ b/tests/unit/PubSub/ResourceNameTraitTest.php @@ -28,35 +28,35 @@ class ResourceNameTraitTest extends \PHPUnit_Framework_TestCase public function setUp() { - $this->trait = $this->getObjectForTrait(ResourceNameTrait::class); + $this->trait = new ResourceNameTraitStub; } public function testPluckProjectId() { - $res = $this->trait->pluckName( + $res = $this->trait->call('pluckName', [ 'project', 'projects/foo' - ); + ]); $this->assertEquals('foo', $res); } public function testPluckTopicName() { - $res = $this->trait->pluckName( + $res = $this->trait->call('pluckName', [ 'topic', 'projects/foo/topics/bar' - ); + ]); $this->assertEquals('bar', $res); } public function testPluckSubscriptionName() { - $res = $this->trait->pluckName( + $res = $this->trait->call('pluckName', [ 'subscription', 'projects/foo/subscriptions/bar' - ); + ]); $this->assertEquals('bar', $res); } @@ -66,26 +66,26 @@ public function testPluckSubscriptionName() */ public function testPluckNameInvalidFormat() { - $this->trait->pluckName('lame', 'bar'); + $this->trait->call('pluckName', ['lame', 'bar']); } public function testFormatProjectId() { - $res = $this->trait->formatName('project', 'foo'); + $res = $this->trait->call('formatName', ['project', 'foo']); $this->assertEquals('projects/foo', $res); } public function testFormatTopicName() { - $res = $this->trait->formatName('topic', 'foo', 'my-project'); + $res = $this->trait->call('formatName', ['topic', 'foo', 'my-project']); $this->assertEquals('projects/my-project/topics/foo', $res); } public function testFormatSubscriptionName() { - $res = $this->trait->formatName('subscription', 'foo', 'my-project'); + $res = $this->trait->call('formatName', ['subscription', 'foo', 'my-project']); $this->assertEquals('projects/my-project/subscriptions/foo', $res); } @@ -95,46 +95,46 @@ public function testFormatSubscriptionName() */ public function testFormatNameInvalidType() { - $this->trait->formatName('lame', ['foo']); + $this->trait->call('formatName', ['lame', 'foo']); } public function testIsFullyQualifiedProjectId() { - $this->assertTrue($this->trait->isFullyQualifiedName( + $this->assertTrue($this->trait->call('isFullyQualifiedName', [ 'project', 'projects/foo' - )); + ])); - $this->assertFalse($this->trait->isFullyQualifiedName( + $this->assertFalse($this->trait->call('isFullyQualifiedName', [ 'project', 'foo' - )); + ])); } public function testIsFullyQualifiedTopicName() { - $this->assertTrue($this->trait->isFullyQualifiedName( + $this->assertTrue($this->trait->call('isFullyQualifiedName', [ 'topic', 'projects/foo/topics/bar' - )); + ])); - $this->assertFalse($this->trait->isFullyQualifiedName( + $this->assertFalse($this->trait->call('isFullyQualifiedName', [ 'topic', 'foo' - )); + ])); } public function testIsFullyQualifiedSubscriptionName() { - $this->assertTrue($this->trait->isFullyQualifiedName( + $this->assertTrue($this->trait->call('isFullyQualifiedName', [ 'subscription', 'projects/foo/subscriptions/bar' - )); + ])); - $this->assertFalse($this->trait->isFullyQualifiedName( + $this->assertFalse($this->trait->call('isFullyQualifiedName', [ 'subscription', 'foo' - )); + ])); } /** @@ -142,6 +142,16 @@ public function testIsFullyQualifiedSubscriptionName() */ public function testIsFullyQualifiedNameInvalidType() { - $this->trait->isFullyQualifiedName('lame', 'foo'); + $this->trait->call('isFullyQualifiedName', ['lame', 'foo']); + } +} + +class ResourceNameTraitStub +{ + use ResourceNameTrait; + + public function call($method, array $args) + { + return call_user_func_array([$this, $method], $args); } } From 85faa54cd9223a22db9f5b34f5e26f425602b6da Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Tue, 28 Feb 2017 09:46:51 -0500 Subject: [PATCH 073/107] Cover snippets (#357) --- dev/src/Snippet/SnippetTestCase.php | 37 ++-- src/Iam/PolicyBuilder.php | 2 +- src/NaturalLanguage/Annotation.php | 10 ++ src/PubSub/Subscription.php | 4 +- tests/snippets/BigQuery/JobTest.php | 18 ++ tests/snippets/BigQuery/QueryResultsTest.php | 26 +++ .../Datastore/DatastoreClientTest.php | 19 ++ tests/snippets/Iam/IamTest.php | 19 +- tests/snippets/Iam/PolicyBuilderTest.php | 10 ++ tests/snippets/Int64Test.php | 52 ++++++ tests/snippets/Logging/PsrLoggerTest.php | 15 ++ .../NaturalLanguage/AnnotationTest.php | 163 ++++++++++++++++++ .../NaturalLanguageClientTest.php | 156 +++++++++++++++++ tests/snippets/PubSub/PubSubClientTest.php | 19 +- tests/snippets/PubSub/SubscriptionTest.php | 2 +- tests/snippets/PubSub/TopicTest.php | 2 +- tests/snippets/ServiceBuilderTest.php | 76 ++++++++ tests/snippets/Speech/OperationTest.php | 26 +-- tests/snippets/Storage/BucketTest.php | 21 +++ tests/snippets/Storage/StorageClientTest.php | 15 ++ tests/snippets/Storage/StorageObjectTest.php | 79 +++++++++ .../Translate/TranslateClientTest.php | 10 +- .../snippets/Vision/Annotation/EntityTest.php | 7 + .../Vision/Annotation/Face/LandmarksTest.php | 4 +- tests/snippets/Vision/Annotation/FaceTest.php | 45 ++++- .../Vision/Annotation/SafeSearchTest.php | 8 + tests/snippets/bootstrap.php | 1 + 27 files changed, 792 insertions(+), 54 deletions(-) create mode 100644 tests/snippets/Int64Test.php create mode 100644 tests/snippets/NaturalLanguage/AnnotationTest.php create mode 100644 tests/snippets/NaturalLanguage/NaturalLanguageClientTest.php create mode 100644 tests/snippets/ServiceBuilderTest.php diff --git a/dev/src/Snippet/SnippetTestCase.php b/dev/src/Snippet/SnippetTestCase.php index 98349768a57d..1bbf1e9ce293 100644 --- a/dev/src/Snippet/SnippetTestCase.php +++ b/dev/src/Snippet/SnippetTestCase.php @@ -26,18 +26,13 @@ */ class SnippetTestCase extends \PHPUnit_Framework_TestCase { - const HOOK_BEFORE = 1000; - const HOOK_AFTER = 1001; + private static $coverage; + private static $parser; - private $coverage; - private $parser; - - public function __construct() + public static function setUpBeforeClass() { - parent::__construct(); - - $this->coverage = Container::$coverage; - $this->parser = Container::$parser; + self::$coverage = Container::$coverage; + self::$parser = Container::$parser; } /** @@ -49,14 +44,14 @@ public function __construct() */ public function snippetFromClass($class, $indexOrName = 0) { - $identifier = $this->parser->createIdentifier($class, $indexOrName); + $identifier = self::$parser->createIdentifier($class, $indexOrName); - $snippet = $this->coverage->cache($identifier); + $snippet = self::$coverage->cache($identifier); if (!$snippet) { - $snippet = $this->parser->classExample($class, $indexOrName); + $snippet = self::$parser->classExample($class, $indexOrName); } - $this->coverage->cover($snippet->identifier()); + self::$coverage->cover($snippet->identifier()); return $snippet; } @@ -73,14 +68,14 @@ public function snippetFromClass($class, $indexOrName = 0) public function snippetFromMagicMethod($class, $method, $indexOrName = 0) { $name = $class .'::'. $method; - $identifier = $this->parser->createIdentifier($name, $indexOrName); + $identifier = self::$parser->createIdentifier($name, $indexOrName); - $snippet = $this->coverage->cache($identifier); + $snippet = self::$coverage->cache($identifier); if (!$snippet) { throw new \Exception('Magic Method '. $name .' was not found'); } - $this->coverage->cover($identifier); + self::$coverage->cover($identifier); return $snippet; } @@ -96,14 +91,14 @@ public function snippetFromMagicMethod($class, $method, $indexOrName = 0) public function snippetFromMethod($class, $method, $indexOrName = 0) { $name = $class .'::'. $method; - $identifier = $this->parser->createIdentifier($name, $indexOrName); + $identifier = self::$parser->createIdentifier($name, $indexOrName); - $snippet = $this->coverage->cache($identifier); + $snippet = self::$coverage->cache($identifier); if (!$snippet) { - $snippet = $this->parser->methodExample($class, $method, $indexOrName); + $snippet = self::$parser->methodExample($class, $method, $indexOrName); } - $this->coverage->cover($identifier); + self::$coverage->cover($identifier); return $snippet; } diff --git a/src/Iam/PolicyBuilder.php b/src/Iam/PolicyBuilder.php index 9a33fa0ecc3a..aab7be22e10e 100644 --- a/src/Iam/PolicyBuilder.php +++ b/src/Iam/PolicyBuilder.php @@ -80,7 +80,7 @@ public function __construct(array $policy = []) * ``` * $builder->setBindings([ * [ - * 'role' => roles/admin', + * 'role' => 'roles/admin', * 'members' => [ * 'user:admin@domain.com' * ] diff --git a/src/NaturalLanguage/Annotation.php b/src/NaturalLanguage/Annotation.php index b14b10fae519..3e0cdd19aaf2 100644 --- a/src/NaturalLanguage/Annotation.php +++ b/src/NaturalLanguage/Annotation.php @@ -34,6 +34,16 @@ * {@see Google\Cloud\NaturalLanguage\NaturalLanguageClient::analyzeSyntax()} and * {@see Google\Cloud\NaturalLanguage\NaturalLanguageClient::annotateText()}. * + * Example: + * ``` + * use Google\Cloud\ServiceBuilder; + * + * $cloud = new ServiceBuilder(); + * $language = $cloud->naturalLanguage(); + * + * $annotation = $language->annotateText('Google Cloud Platform is a powerful tool.'); + * ``` + * * @method sentences() { * Returns an array of sentences found in the document. * diff --git a/src/PubSub/Subscription.php b/src/PubSub/Subscription.php index 4dcf3910ceab..95608d7d95f9 100644 --- a/src/PubSub/Subscription.php +++ b/src/PubSub/Subscription.php @@ -479,14 +479,14 @@ public function modifyAckDeadline(Message $message, $seconds, array $options = [ * $messages = $subscription->pull(); * $messagesArray = iterator_to_array($messages); * - * // Set the ack deadline to a minute and a half from now for every message + * // Set the ack deadline to three seconds from now for every message * $subscription->modifyAckDeadlineBatch($messagesArray, 3); * * // Delay execution, or make a sandwich or something. * sleep(2); * * // Now we'll acknowledge - * $subscription->acknowledge($messagesArray); + * $subscription->acknowledgeBatch($messagesArray); * ``` * * @codingStandardsIgnoreStart diff --git a/tests/snippets/BigQuery/JobTest.php b/tests/snippets/BigQuery/JobTest.php index 7cb1ab1a7496..18f1f070174b 100644 --- a/tests/snippets/BigQuery/JobTest.php +++ b/tests/snippets/BigQuery/JobTest.php @@ -150,4 +150,22 @@ public function testReload() $snippet->replace('sleep(1);', ''); $snippet->invoke(); } + + public function testId() + { + $snippet = $this->snippetFromMethod(Job::class, 'id'); + $snippet->addLocal('job', $this->getJob($this->connection, [])); + $res = $snippet->invoke(); + + $this->assertEquals($res->output(), $this->identity['jobId']); + } + + public function testIdentity() + { + $snippet = $this->snippetFromMethod(Job::class, 'identity'); + $snippet->addLocal('job', $this->getJob($this->connection, [])); + $res = $snippet->invoke(); + + $this->assertEquals($res->output(), $this->identity['projectId']); + } } diff --git a/tests/snippets/BigQuery/QueryResultsTest.php b/tests/snippets/BigQuery/QueryResultsTest.php index cb4398fff155..f79b7450a992 100644 --- a/tests/snippets/BigQuery/QueryResultsTest.php +++ b/tests/snippets/BigQuery/QueryResultsTest.php @@ -39,6 +39,7 @@ class QueryResultsTest extends SnippetTestCase public function setUp() { $this->info = [ + 'totalBytesProcessed' => 3, 'jobComplete' => false, 'jobReference' => [ 'jobId' => 'job' @@ -112,4 +113,29 @@ public function testIdentity() $res = $snippet->invoke(); $this->assertEquals(self::PROJECT, $res->output()); } + + public function testInfo() + { + $snippet = $this->snippetFromMethod(QueryResults::class, 'info'); + $snippet->addLocal('queryResults', $this->qr); + + $res = $snippet->invoke(); + $this->assertEquals($this->info['totalBytesProcessed'], $res->output()); + } + + public function testReload() + { + $this->connection->getQueryResults(Argument::any()) + ->shouldBeCalled() + ->willReturn(['jobComplete' => true] + $this->info); + + $this->qr->setConnection($this->connection->reveal()); + + $snippet = $this->snippetFromMethod(QueryResults::class, 'reload'); + $snippet->addLocal('queryResults', $this->qr); + $snippet->replace('sleep(1);', ''); + + $res = $snippet->invoke(); + $this->assertEquals('Query complete!', $res->output()); + } } diff --git a/tests/snippets/Datastore/DatastoreClientTest.php b/tests/snippets/Datastore/DatastoreClientTest.php index fb5482d01499..54037599843d 100644 --- a/tests/snippets/Datastore/DatastoreClientTest.php +++ b/tests/snippets/Datastore/DatastoreClientTest.php @@ -30,6 +30,7 @@ use Google\Cloud\Datastore\Query\QueryInterface; use Google\Cloud\Datastore\Transaction; use Google\Cloud\Dev\Snippet\SnippetTestCase; +use Google\Cloud\Int64; use Prophecy\Argument; /** @@ -234,6 +235,24 @@ public function testBlob() { $snippet = $this->snippetFromMethod(DatastoreClient::class, 'blob'); $snippet->addLocal('datastore', $this->client); + + $res = $snippet->invoke('blob'); + $this->assertInstanceOf(Blob::class, $res->returnVal()); + } + + public function testInt64() + { + $snippet = $this->snippetFromMethod(DatastoreClient::class, 'int64'); + $snippet->addLocal('datastore', $this->client); + + $res = $snippet->invoke('int64'); + $this->assertInstanceOf(Int64::class, $res->returnVal()); + } + + public function testBlobWithFile() + { + $snippet = $this->snippetFromMethod(DatastoreClient::class, 'blob', 1); + $snippet->addLocal('datastore', $this->client); $snippet->replace("file_get_contents(__DIR__ .'/family-photo.jpg')", "''"); $res = $snippet->invoke('blob'); diff --git a/tests/snippets/Iam/IamTest.php b/tests/snippets/Iam/IamTest.php index 0f9602a4390e..d66fe0bf8890 100644 --- a/tests/snippets/Iam/IamTest.php +++ b/tests/snippets/Iam/IamTest.php @@ -43,6 +43,14 @@ public function setUp() $this->iam->setConnection($this->connection->reveal()); } + public function testClass() + { + $snippet = $this->snippetFromClass(Iam::class); + $res = $snippet->invoke('iam'); + + $this->assertInstanceOf(Iam::class, $res->returnVal()); + } + public function testPolicy() { $snippet = $this->snippetFromMethod(Iam::class, 'policy'); @@ -62,7 +70,7 @@ public function testPolicy() $this->assertEquals('foo', $res->returnVal()); } - public function setPolicy() + public function testSetPolicy() { $snippet = $this->snippetFromMethod(Iam::class, 'setPolicy'); $snippet->addLocal('iam', $this->iam); @@ -76,9 +84,12 @@ public function setPolicy() ]); $this->connection->setPolicy([ - 'bindings' => [ - ['members' => ['user:test@example.com']] - ] + 'policy' => [ + 'bindings' => [ + ['members' => 'user:test@example.com'] + ] + ], + 'resource' => $this->resource ])->shouldBeCalled()->willReturn('foo'); $this->iam->setConnection($this->connection->reveal()); diff --git a/tests/snippets/Iam/PolicyBuilderTest.php b/tests/snippets/Iam/PolicyBuilderTest.php index db7b94aa0bb6..6649bb0c6e3b 100644 --- a/tests/snippets/Iam/PolicyBuilderTest.php +++ b/tests/snippets/Iam/PolicyBuilderTest.php @@ -43,6 +43,16 @@ public function testClass() } public function testSetBindings() + { + $snippet = $this->snippetFromMethod(PolicyBuilder::class, 'setBindings'); + $snippet->addLocal('builder', $this->pb); + + $res = $snippet->invoke(); + $this->assertEquals('roles/admin', $this->pb->result()['bindings'][0]['role']); + $this->assertEquals('user:admin@domain.com', $this->pb->result()['bindings'][0]['members'][0]); + } + + public function testAddBindings() { $snippet = $this->snippetFromMethod(PolicyBuilder::class, 'addBinding'); $snippet->addLocal('builder', $this->pb); diff --git a/tests/snippets/Int64Test.php b/tests/snippets/Int64Test.php new file mode 100644 index 000000000000..ee4f4427a6ab --- /dev/null +++ b/tests/snippets/Int64Test.php @@ -0,0 +1,52 @@ +int64 = new Int64((string)self::VALUE); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(Int64::class); + $snippet->addUse(Int64::class); + $this->assertInstanceOf(Int64::class, $snippet->invoke('int64')->returnVal()); + } + + public function testGet() + { + $snippet = $this->snippetFromMethod(Int64::class, 'get'); + $snippet->addLocal('int64', $this->int64); + $res = $snippet->invoke('value'); + + $this->assertEquals((string)self::VALUE, $res->returnVal()); + } +} diff --git a/tests/snippets/Logging/PsrLoggerTest.php b/tests/snippets/Logging/PsrLoggerTest.php index 3fb0a451976d..a33a505779cb 100644 --- a/tests/snippets/Logging/PsrLoggerTest.php +++ b/tests/snippets/Logging/PsrLoggerTest.php @@ -64,6 +64,21 @@ public function testEmergency() $snippet->invoke(); } + public function testAlert() + { + $snippet = $this->snippetFromMethod(PsrLogger::class, 'alert'); + $snippet->addLocal('psrLogger', $this->psr); + + $this->connection->writeEntries(Argument::that(function ($args) { + if ($args['entries'][0]['severity'] !== Logger::ALERT) return false; + return true; + }))->shouldBeCalled(); + + $this->psr->setConnection($this->connection->reveal()); + + $snippet->invoke(); + } + public function testCritical() { $snippet = $this->snippetFromMethod(PsrLogger::class, 'critical'); diff --git a/tests/snippets/NaturalLanguage/AnnotationTest.php b/tests/snippets/NaturalLanguage/AnnotationTest.php new file mode 100644 index 000000000000..cc37edc7e823 --- /dev/null +++ b/tests/snippets/NaturalLanguage/AnnotationTest.php @@ -0,0 +1,163 @@ +info = [ + 'sentences' => [ + [ + 'text' => [ + 'content' => 'hello world' + ] + ] + ], + 'tokens' => [ + [ + 'text' => [ + 'content' => 'hello world' + ], + 'partOfSpeech' => [ + 'tag' => 'NOUN' + ], + 'dependencyEdge' => [ + 'label' => 'P' + ], + 'lemma' => 'foo' + ] + ], + 'entities' => [ + [ + 'type' => 'PERSON', + 'name' => 'somebody' + ] + ], + 'language' => 'en-us', + 'documentSentiment' => [ + 'score' => 999 + ] + ]; + $this->annotation = new Annotation($this->info); + } + + public function testClass() + { + $connection = $this->prophesize(ConnectionInterface::class); + $connection->annotateText(Argument::any()) + ->shouldBeCalled() + ->willReturn([]); + + $snippet = $this->snippetFromClass(Annotation::class); + $snippet->addLocal('connectionStub', $connection->reveal()); + $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($language); + $property = $reflection->getProperty(\'connection\'); + $property->setAccessible(true); + $property->setValue($language, $connectionStub); + $property->setAccessible(false);' + ); + + $res = $snippet->invoke('annotation'); + $this->assertInstanceOf(Annotation::class, $res->returnVal()); + } + + public function testSentences() + { + $snippet = $this->snippetFromMagicMethod(Annotation::class, 'sentences'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke(); + $this->assertEquals($this->info['sentences'][0]['text']['content'], $res->output()); + } + + public function testTokens() + { + $snippet = $this->snippetFromMagicMethod(Annotation::class, 'tokens'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke(); + $this->assertEquals($this->info['tokens'][0]['text']['content'], $res->output()); + } + + public function testEntities() + { + $snippet = $this->snippetFromMagicMethod(Annotation::class, 'entities'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke(); + $this->assertEquals($this->info['entities'][0]['type'], $res->output()); + } + + public function testLanguage() + { + $snippet = $this->snippetFromMagicMethod(Annotation::class, 'language'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke(); + $this->assertEquals($this->info['language'], $res->output()); + } + + public function testInfo() + { + $snippet = $this->snippetFromMethod(Annotation::class, 'info'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke('info'); + + $this->assertEquals($this->info, $res->returnVal()); + } + + public function testSentiment() + { + $snippet = $this->snippetFromMethod(Annotation::class, 'sentiment'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke(); + $this->assertEquals('This is a positive message.', $res->output()); + } + + public function testTokensByTag() + { + $snippet = $this->snippetFromMethod(Annotation::class, 'tokensByTag'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke(); + $this->assertEquals($this->info['tokens'][0]['lemma'], $res->output()); + } + + public function testTokensByLabel() + { + $snippet = $this->snippetFromMethod(Annotation::class, 'tokensByLabel'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke(); + $this->assertEquals($this->info['tokens'][0]['lemma'], $res->output()); + } + + public function testEntitiesByType() + { + $snippet = $this->snippetFromMethod(Annotation::class, 'entitiesByType'); + $snippet->addLocal('annotation', $this->annotation); + $res = $snippet->invoke(); + $this->assertEquals($this->info['entities'][0]['name'], $res->output()); + } +} diff --git a/tests/snippets/NaturalLanguage/NaturalLanguageClientTest.php b/tests/snippets/NaturalLanguage/NaturalLanguageClientTest.php new file mode 100644 index 000000000000..123fdd8f11f5 --- /dev/null +++ b/tests/snippets/NaturalLanguage/NaturalLanguageClientTest.php @@ -0,0 +1,156 @@ +connection = $this->prophesize(ConnectionInterface::class); + $this->client = new \NaturalLanguageClientStub; + $this->client->setConnection($this->connection->reveal()); + } + + public function testClass() + { + $snippet = $this->snippetFromClass(NaturalLanguageClient::class); + $res = $snippet->invoke('language'); + $this->assertInstanceOf(NaturalLanguageClient::class, $res->returnVal()); + } + + public function testClassDirect() + { + $snippet = $this->snippetFromClass(NaturalLanguageClient::class, 1); + $res = $snippet->invoke('language'); + $this->assertInstanceOf(NaturalLanguageClient::class, $res->returnVal()); + } + + public function testAnalyzeEntities() + { + $this->connection->analyzeEntities(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'entities' => [ + [ + 'type' => 'PERSON' + ] + ] + ]); + + $this->client->setConnection($this->connection->reveal()); + + $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'analyzeEntities'); + $snippet->addLocal('language', $this->client); + + $res = $snippet->invoke(); + $this->assertEquals('PERSON', $res->output()); + } + + public function testAnalyzeSentiment() + { + $this->connection->analyzeSentiment(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'documentSentiment' => [ + 'score' => 1.0 + ] + ]); + + $this->client->setConnection($this->connection->reveal()); + + $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'analyzeSentiment'); + $snippet->addLocal('language', $this->client); + + $res = $snippet->invoke(); + $this->assertEquals("This is a positive message.", $res->output()); + } + + public function testAnalyzeSyntax() + { + $this->connection->analyzeSyntax(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'sentences' => [ + [ + 'text' => [ + 'beginOffset' => 1.0 + ] + ] + ] + ]); + + $this->client->setConnection($this->connection->reveal()); + + $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'analyzeSyntax'); + $snippet->addLocal('language', $this->client); + + $res = $snippet->invoke(); + $this->assertEquals('1.0', $res->output()); + } + + public function testAnnotateTextAllFeatures() + { + $this->connection->annotateText(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'documentSentiment' => [ + 'magnitude' => 999 + ] + ]); + + $this->client->setConnection($this->connection->reveal()); + + $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'annotateText'); + $snippet->addLocal('language', $this->client); + + $this->assertEquals('999', $snippet->invoke()->output()); + } + + public function testAnnotateTextSomeFeatures() + { + $this->connection->annotateText(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'tokens' => [ + [ + 'text' => [ + 'beginOffset' => '2.0' + ] + ] + ] + ]); + + $this->client->setConnection($this->connection->reveal()); + + $snippet = $this->snippetFromMethod(NaturalLanguageClient::class, 'annotateText', 1); + $snippet->addLocal('language', $this->client); + + $this->assertEquals('2.0', $snippet->invoke()->output()); + } +} diff --git a/tests/snippets/PubSub/PubSubClientTest.php b/tests/snippets/PubSub/PubSubClientTest.php index 5f8969ef8c6d..8fd768c1d715 100644 --- a/tests/snippets/PubSub/PubSubClientTest.php +++ b/tests/snippets/PubSub/PubSubClientTest.php @@ -45,7 +45,24 @@ public function setUp() public function testClassExample1() { - $snippet = $this->snippetFromClass(PubSubClient::class, '__construct'); + $snippet = $this->snippetFromClass(PubSubClient::class); + $res = $snippet->invoke('pubsub'); + + $this->assertInstanceOf(PubSubClient::class, $res->returnVal()); + } + + public function testClassExample2() + { + $snippet = $this->snippetFromClass(PubSubClient::class, 1); + $res = $snippet->invoke('pubsub'); + + $this->assertInstanceOf(PubSubClient::class, $res->returnVal()); + } + + // phpunit doesn't get the value of $_ENV, so testing PUBSUB_EMULATOR_HOST is pretty tough. + public function testClassExample3() + { + $snippet = $this->snippetFromClass(PubSubClient::class, 2); $res = $snippet->invoke('pubsub'); $this->assertInstanceOf(PubSubClient::class, $res->returnVal()); diff --git a/tests/snippets/PubSub/SubscriptionTest.php b/tests/snippets/PubSub/SubscriptionTest.php index 04684afd8eb8..e877fa061a3c 100644 --- a/tests/snippets/PubSub/SubscriptionTest.php +++ b/tests/snippets/PubSub/SubscriptionTest.php @@ -241,7 +241,7 @@ public function testModifyAckDeadline() public function testModifyAckDeadlineBatch() { - $snippet = $this->snippetFromMethod(Subscription::class, 'modifyAckDeadline'); + $snippet = $this->snippetFromMethod(Subscription::class, 'modifyAckDeadlineBatch'); $snippet->addLocal('subscription', $this->subscription); $this->connection->pull(Argument::any()) diff --git a/tests/snippets/PubSub/TopicTest.php b/tests/snippets/PubSub/TopicTest.php index aed6f638b7aa..c7e7230af0a7 100644 --- a/tests/snippets/PubSub/TopicTest.php +++ b/tests/snippets/PubSub/TopicTest.php @@ -224,7 +224,7 @@ public function testSubscriptions() $this->assertEquals(self::SUBSCRIPTION, $res->output()); } - public function iam() + public function testIam() { $snippet = $this->snippetFromMethod(Topic::class, 'iam'); $snippet->addLocal('topic', $this->topic); diff --git a/tests/snippets/ServiceBuilderTest.php b/tests/snippets/ServiceBuilderTest.php new file mode 100644 index 000000000000..7443510ada73 --- /dev/null +++ b/tests/snippets/ServiceBuilderTest.php @@ -0,0 +1,76 @@ +cloud = new ServiceBuilder; + } + + public function testConstructor() + { + $snippet = $this->snippetFromMethod(ServiceBuilder::class, '__construct'); + $this->assertInstanceOf(ServiceBuilder::class, $snippet->invoke('cloud')->returnVal()); + } + + public function serviceBuilderMethods() + { + return [ + ['bigQuery', BigQueryClient::class, 'bigQuery'], + ['datastore', DatastoreClient::class, 'datastore'], + ['logging', LoggingClient::class, 'logging'], + ['naturalLanguage', NaturalLanguageClient::class, 'language'], + ['pubsub', PubSubClient::class, 'pubsub'], + ['speech', SpeechClient::class, 'speech'], + ['storage', StorageClient::class, 'storage'], + ['vision', VisionClient::class, 'vision'], + ['translate', TranslateClient::class, 'translate'] + ]; + } + + /** + * @dataProvider serviceBuilderMethods + */ + public function testServices($method, $returnType, $returnName) + { + $snippet = $this->snippetFromMethod(ServiceBuilder::class, $method); + $snippet->addLocal('cloud', $this->cloud); + $res = $snippet->invoke($returnName); + + $this->assertInstanceOf($returnType, $res->returnVal()); + } +} diff --git a/tests/snippets/Speech/OperationTest.php b/tests/snippets/Speech/OperationTest.php index 0ac7b04ecb59..9257ac1b7c3a 100644 --- a/tests/snippets/Speech/OperationTest.php +++ b/tests/snippets/Speech/OperationTest.php @@ -50,24 +50,24 @@ public function setUp() $this->operation = new \SpeechOperationStub($this->connection->reveal(), $this->opData['name'], $this->opData); } - // /** - // * @expectedException InvalidArgumentException - // */ - // public function testClass() - // { - // $snippet = $this->snippetFromClass(Operation::class); + /** + * @expectedException InvalidArgumentException + */ + public function testClass() + { + $snippet = $this->snippetFromClass(Operation::class); - // $connectionStub = $this->prophesize(ConnectionInterface::class); + $connectionStub = $this->prophesize(ConnectionInterface::class); - // $connectionStub->asyncRecognize(Argument::any()) - // ->willReturn(['name' => 'foo']); + $connectionStub->asyncRecognize(Argument::any()) + ->willReturn(['name' => 'foo']); - // $snippet->addLocal('connectionStub', $connectionStub->reveal()); + $snippet->addLocal('connectionStub', $connectionStub->reveal()); - // $snippet->setLine(5, '$audioFileStream = fopen(\'php://temp\', \'r\');'); + $snippet->replace("__DIR__ . '/audio.flac'", '"php://temp"'); - // $res = $snippet->invoke('operation'); - // } + $res = $snippet->invoke('operation'); + } public function testIsComplete() { diff --git a/tests/snippets/Storage/BucketTest.php b/tests/snippets/Storage/BucketTest.php index c0bd6a6a4994..563c5b0061eb 100644 --- a/tests/snippets/Storage/BucketTest.php +++ b/tests/snippets/Storage/BucketTest.php @@ -25,6 +25,7 @@ use Google\Cloud\Storage\StorageObject; use Google\Cloud\Upload\MultipartUploader; use Google\Cloud\Upload\ResumableUploader; +use Google\Cloud\Upload\StreamableUploader; use Prophecy\Argument; /** @@ -196,6 +197,26 @@ public function testGetResumableUploader() $res = $snippet->invoke('object'); } + public function testGetStreamableUploader() + { + $snippet = $this->snippetFromMethod(Bucket::class, 'getStreamableUploader'); + $snippet->addLocal('bucket', $this->bucket); + $snippet->addUse(GoogleException::class); + $snippet->replace("data.txt", 'php://temp'); + + $uploader = $this->prophesize(StreamableUploader::class); + $uploader->upload() + ->shouldBeCalledTimes(1); + + $this->connection->insertObject(Argument::any()) + ->shouldBeCalled() + ->willReturn($uploader->reveal()); + + $this->bucket->setConnection($this->connection->reveal()); + + $res = $snippet->invoke(); + } + public function testObject() { $snippet = $this->snippetFromMethod(Bucket::class, 'object'); diff --git a/tests/snippets/Storage/StorageClientTest.php b/tests/snippets/Storage/StorageClientTest.php index da195fc1d820..0c339bda8b3c 100644 --- a/tests/snippets/Storage/StorageClientTest.php +++ b/tests/snippets/Storage/StorageClientTest.php @@ -123,4 +123,19 @@ public function testCreateBucket() $res = $snippet->invoke('bucket'); $this->assertInstanceOf(Bucket::class, $res->returnVal()); } + + public function testCreateBucketWithLogging() + { + $snippet = $this->snippetFromMethod(StorageClient::class, 'createBucket', 1); + $snippet->addLocal('storage', $this->client); + + $this->connection->insertBucket(Argument::any()) + ->shouldBeCalled() + ->willReturn([]); + + $this->client->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('bucket'); + $this->assertInstanceOf(Bucket::class, $res->returnVal()); + } } diff --git a/tests/snippets/Storage/StorageObjectTest.php b/tests/snippets/Storage/StorageObjectTest.php index 541822622455..c94204e3375f 100644 --- a/tests/snippets/Storage/StorageObjectTest.php +++ b/tests/snippets/Storage/StorageObjectTest.php @@ -19,7 +19,9 @@ use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Storage\Acl; +use Google\Cloud\Storage\Bucket; use Google\Cloud\Storage\Connection\ConnectionInterface; +use Google\Cloud\Storage\StorageClient; use Google\Cloud\Storage\StorageObject; use Prophecy\Argument; use Psr\Http\Message\StreamInterface; @@ -121,10 +123,87 @@ public function testCopy() $this->assertInstanceOf(StorageObject::class, $res->returnVal()); } + public function testCopyToBucket() + { + $bucket = $this->prophesize(Bucket::class); + $bucket->name()->willReturn('foo'); + + $storage = $this->prophesize(StorageClient::class); + $storage->bucket(Argument::any()) + ->willReturn($bucket->reveal()); + + $snippet = $this->snippetFromMethod(StorageObject::class, 'copy', 1); + $snippet->addLocal('object', $this->object); + $snippet->addLocal('storage', $storage->reveal()); + + $this->connection->copyObject(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => 'New Object', + 'bucket' => self::BUCKET, + 'generation' => 'foo' + ]); + + $this->object->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('copiedObject'); + $this->assertInstanceOf(StorageObject::class, $res->returnVal()); + } + public function testRewrite() { $snippet = $this->snippetFromMethod(StorageObject::class, 'rewrite'); $snippet->addLocal('object', $this->object); + + $this->connection->rewriteObject(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'resource' => [ + 'name' => self::OBJECT, + 'bucket' => self::BUCKET, + 'generation' => 'foo' + ] + ]); + + $this->object->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('rewrittenObject'); + $this->assertInstanceOf(StorageObject::class, $res->returnVal()); + } + + public function testRewriteNewObjectName() + { + $bucket = $this->prophesize(Bucket::class); + $bucket->name()->willReturn('foo'); + + $storage = $this->prophesize(StorageClient::class); + $storage->bucket(Argument::any()) + ->willReturn($bucket->reveal()); + + $snippet = $this->snippetFromMethod(StorageObject::class, 'rewrite', 1); + $snippet->addLocal('storage', $storage->reveal()); + $snippet->addLocal('object', $this->object); + + $this->connection->rewriteObject(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'resource' => [ + 'name' => self::OBJECT, + 'bucket' => self::BUCKET, + 'generation' => 'foo' + ] + ]); + + $this->object->setConnection($this->connection->reveal()); + + $res = $snippet->invoke('rewrittenObject'); + $this->assertInstanceOf(StorageObject::class, $res->returnVal()); + } + + public function testRewriteNewKey() + { + $snippet = $this->snippetFromMethod(StorageObject::class, 'rewrite', 2); + $snippet->addLocal('object', $this->object); $snippet->replace("file_get_contents(__DIR__ . '/key.txt')", "'testKeyData'"); $this->connection->rewriteObject(Argument::any()) diff --git a/tests/snippets/Translate/TranslateClientTest.php b/tests/snippets/Translate/TranslateClientTest.php index 05190e750867..7ec952b8bc4f 100644 --- a/tests/snippets/Translate/TranslateClientTest.php +++ b/tests/snippets/Translate/TranslateClientTest.php @@ -37,7 +37,7 @@ public function setUp() $this->client->setConnection($this->connection->reveal()); } - public function testTrue() + public function testClass() { $snippet = $this->snippetFromClass(TranslateClient::class); $res = $snippet->invoke('translate'); @@ -45,6 +45,14 @@ public function testTrue() $this->assertInstanceOf(TranslateClient::class, $res->returnVal()); } + public function testClassDirectInstantiation() + { + $snippet = $this->snippetFromClass(TranslateClient::class, 1); + $res = $snippet->invoke('translate'); + + $this->assertInstanceOf(TranslateClient::class, $res->returnVal()); + } + public function testTranslate() { $snippet = $this->snippetFromMethod(TranslateClient::class, 'translate'); diff --git a/tests/snippets/Vision/Annotation/EntityTest.php b/tests/snippets/Vision/Annotation/EntityTest.php index 2e0ff26232e1..b19d7b2268dd 100644 --- a/tests/snippets/Vision/Annotation/EntityTest.php +++ b/tests/snippets/Vision/Annotation/EntityTest.php @@ -74,6 +74,13 @@ public function testClass() $this->assertInstanceOf(Entity::class, $res->returnVal()); } + public function testInfo() + { + $snippet = $this->snippetFromMagicMethod(Entity::class, 'info'); + $snippet->addLocal('text', $this->entity); + $this->assertEquals($this->entityData, $snippet->invoke('info')->returnVal()); + } + public function testMid() { $snippet = $this->snippetFromMagicMethod(Entity::class, 'mid'); diff --git a/tests/snippets/Vision/Annotation/Face/LandmarksTest.php b/tests/snippets/Vision/Annotation/Face/LandmarksTest.php index b70e41fed9ec..9dc833cf7993 100644 --- a/tests/snippets/Vision/Annotation/Face/LandmarksTest.php +++ b/tests/snippets/Vision/Annotation/Face/LandmarksTest.php @@ -153,7 +153,7 @@ public function testLeftEyeBoundaries() public function testLeftEyeBrow() { - $snippet = $this->snippetFromMethod(Landmarks::class, 'leftEyeBrow'); + $snippet = $this->snippetFromMethod(Landmarks::class, 'leftEyebrow'); $snippet->addLocal('landmarks', $this->landmarks); $res = $snippet->invoke(); @@ -198,7 +198,7 @@ public function testRightEyeBoundaries() public function testRightEyeBrow() { - $snippet = $this->snippetFromMethod(Landmarks::class, 'rightEyeBrow'); + $snippet = $this->snippetFromMethod(Landmarks::class, 'rightEyebrow'); $snippet->addLocal('landmarks', $this->landmarks); $res = $snippet->invoke(); diff --git a/tests/snippets/Vision/Annotation/FaceTest.php b/tests/snippets/Vision/Annotation/FaceTest.php index 9e9ade0a923a..58dec319541c 100644 --- a/tests/snippets/Vision/Annotation/FaceTest.php +++ b/tests/snippets/Vision/Annotation/FaceTest.php @@ -41,13 +41,13 @@ public function setUp() "tiltAngle" => 'testtiltAngle', "detectionConfidence" => 'testdetectionConfidence', "landmarkingConfidence" => 'testlandmarkingConfidence', - "joyLikelihood" => 'testjoyLikelihood', - "sorrowLikelihood" => 'testsorrowLikelihood', - "angerLikelihood" => 'testangerLikelihood', - "surpriseLikelihood" => 'testsurpriseLikelihood', - "underExposedLikelihood" => 'testunderExposedLikelihood', - "blurredLikelihood" => 'testblurredLikelihood', - "headwearLikelihood" => 'testheadwearLikelihood', + "joyLikelihood" => 'VERY_LIKELY', + "sorrowLikelihood" => 'VERY_LIKELY', + "angerLikelihood" => 'VERY_LIKELY', + "surpriseLikelihood" => 'VERY_LIKELY', + "underExposedLikelihood" => 'VERY_LIKELY', + "blurredLikelihood" => 'VERY_LIKELY', + "headwearLikelihood" => 'VERY_LIKELY', ]; $this->face = new Face($this->faceData); @@ -83,6 +83,13 @@ public function testClass() ); } + public function testInfo() + { + $snippet = $this->snippetFromMagicMethod(Face::class, 'info'); + $snippet->addLocal('face', $this->face); + $this->assertEquals($this->faceData, $snippet->invoke('info')->returnVal()); + } + public function testLandmarks() { $snippet = $this->snippetFromMagicMethod(Face::class, 'landmarks'); @@ -225,4 +232,28 @@ public function testHeadwearlikelihood() $this->assertEquals($this->faceData['headwearLikelihood'], $res->output()); } + /** + * @dataProvider boolTests + */ + public function testFaceBoolTests($method, $output) + { + $snippet = $this->snippetFromMethod(Face::class, $method); + $snippet->addLocal('face', $this->face); + + $res = $snippet->invoke(); + $this->assertEquals($output, $res->output()); + } + + public function boolTests() + { + return [ + ['isJoyful', 'Face is Joyful'], + ['isSorrowful', 'Face is Sorrowful'], + ['isAngry', 'Face is Angry'], + ['isSurprised', 'Face is Surprised'], + ['isUnderExposed', 'Face is Under Exposed'], + ['isBlurred', 'Face is Blurred'], + ['hasHeadwear', 'Face has Headwear'] + ]; + } } diff --git a/tests/snippets/Vision/Annotation/SafeSearchTest.php b/tests/snippets/Vision/Annotation/SafeSearchTest.php index ac04eddb928f..e9695f1dde87 100644 --- a/tests/snippets/Vision/Annotation/SafeSearchTest.php +++ b/tests/snippets/Vision/Annotation/SafeSearchTest.php @@ -70,6 +70,14 @@ public function testClass() $this->assertInstanceOf(SafeSearch::class, $res->returnVal()); } + public function testInfo() + { + $snippet = $this->snippetFromMagicMethod(SafeSearch::class, 'info'); + $snippet->addLocal('safeSearch', $this->ss); + + $this->assertEquals($this->ssData, $snippet->invoke('info')->returnVal()); + } + public function testAdult() { $snippet = $this->snippetFromMagicMethod(SafeSearch::class, 'adult'); diff --git a/tests/snippets/bootstrap.php b/tests/snippets/bootstrap.php index af9eec686e44..1b6d6c687a58 100644 --- a/tests/snippets/bootstrap.php +++ b/tests/snippets/bootstrap.php @@ -48,6 +48,7 @@ function stub($name, $extends) stub('LoggerStub', Google\Cloud\Logging\Logger::class); stub('LoggingClientStub', Google\Cloud\Logging\LoggingClient::class); stub('MetricStub', Google\Cloud\Logging\Metric::class); +stub('NaturalLanguageClientStub', Google\Cloud\NaturalLanguage\NaturalLanguageClient::class); stub('OperationStub', Google\Cloud\Datastore\Operation::class); stub('PubSubClientStub', Google\Cloud\PubSub\PubSubClient::class); stub('QueryResultsStub', Google\Cloud\BigQuery\QueryResults::class); From 4e534db1463f802add28c74dd93c57c799852ab9 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 2 Mar 2017 11:09:37 -0500 Subject: [PATCH 074/107] Vision v1.1 (#365) * Implement Vision against http URI * Add support for DOCUMENT_TEXT_DETECTION * Implement Crop Hints * Implement WebAnnotation * Fix return types, add helper for full text annotation * Add unit tests * Add snippet tests and cleanup * Code review fixes * Add system tests, add Document type and relevant tests * Use better URL detection * Add test for URL scheme * Update docs * Fix mistake * Add type hints to constructors * Update exception * Change WEB_ANNOTATION to WEB_DETECTION * Change WEB_ANNOTATION to WEB_DETECTION (#36) * Update VisionClient class description * Remove duplicate test --- docs/toc.json | 32 +++- src/Vision/Annotation.php | 98 +++++++++- src/Vision/Annotation/CropHint.php | 83 +++++++++ src/Vision/Annotation/Document.php | 81 +++++++++ src/Vision/Annotation/Entity.php | 2 +- src/Vision/Annotation/Face.php | 2 +- src/Vision/Annotation/Face/Landmarks.php | 2 +- src/Vision/Annotation/ImageProperties.php | 2 +- src/Vision/Annotation/SafeSearch.php | 2 +- src/Vision/Annotation/Web.php | 170 ++++++++++++++++++ src/Vision/Annotation/Web/WebEntity.php | 85 +++++++++ src/Vision/Annotation/Web/WebImage.php | 75 ++++++++ src/Vision/Annotation/Web/WebPage.php | 75 ++++++++ .../ServiceDefinition/vision-v1.json | 14 +- src/Vision/Image.php | 72 +++++--- src/Vision/VisionClient.php | 23 +-- tests/snippets/Speech/SpeechClientTest.php | 2 +- .../Vision/Annotation/CropHintTest.php | 102 +++++++++++ .../Vision/Annotation/DocumentTest.php | 98 ++++++++++ .../snippets/Vision/Annotation/EntityTest.php | 6 +- .../Vision/Annotation/Face/LandmarksTest.php | 5 +- tests/snippets/Vision/Annotation/FaceTest.php | 5 +- .../Vision/Annotation/ImagePropertiesTest.php | 5 +- .../Vision/Annotation/SafeSearchTest.php | 25 ++- .../Vision/Annotation/Web/WebEntityTest.php | 103 +++++++++++ .../Vision/Annotation/Web/WebImageTest.php | 93 ++++++++++ .../Vision/Annotation/Web/WebPageTest.php | 93 ++++++++++ tests/snippets/Vision/Annotation/WebTest.php | 120 +++++++++++++ tests/snippets/Vision/AnnotationTest.php | 43 ++++- tests/snippets/Vision/ImageTest.php | 20 ++- tests/snippets/Vision/VisionClientTest.php | 37 +++- tests/snippets/bootstrap.php | 10 +- tests/system/Vision/AnnotationsTest.php | 169 +++++++++++++++++ tests/system/Vision/VisionTestCase.php | 47 +++++ tests/system/Vision/fixtures/google.jpg | Bin 0 -> 3098 bytes tests/system/Vision/fixtures/landmark.jpg | Bin 0 -> 19316 bytes tests/system/Vision/fixtures/obama.jpg | Bin 0 -> 18809 bytes tests/system/Vision/fixtures/text.jpg | Bin 0 -> 7842 bytes tests/unit/Vision/Annotation/CropHintTest.php | 55 ++++++ tests/unit/Vision/Annotation/DocumentTest.php | 38 ++++ .../Vision/Annotation/Web/WebEntityTest.php | 54 ++++++ .../Vision/Annotation/Web/WebImageTest.php | 48 +++++ .../Vision/Annotation/Web/WebPageTest.php | 48 +++++ tests/unit/Vision/Annotation/WebTest.php | 75 ++++++++ tests/unit/Vision/AnnotationTest.php | 11 +- tests/unit/Vision/ImageTest.php | 54 +++++- 46 files changed, 2087 insertions(+), 97 deletions(-) create mode 100644 src/Vision/Annotation/CropHint.php create mode 100644 src/Vision/Annotation/Document.php create mode 100644 src/Vision/Annotation/Web.php create mode 100644 src/Vision/Annotation/Web/WebEntity.php create mode 100644 src/Vision/Annotation/Web/WebImage.php create mode 100644 src/Vision/Annotation/Web/WebPage.php create mode 100644 tests/snippets/Vision/Annotation/CropHintTest.php create mode 100644 tests/snippets/Vision/Annotation/DocumentTest.php create mode 100644 tests/snippets/Vision/Annotation/Web/WebEntityTest.php create mode 100644 tests/snippets/Vision/Annotation/Web/WebImageTest.php create mode 100644 tests/snippets/Vision/Annotation/Web/WebPageTest.php create mode 100644 tests/snippets/Vision/Annotation/WebTest.php create mode 100644 tests/system/Vision/AnnotationsTest.php create mode 100644 tests/system/Vision/VisionTestCase.php create mode 100644 tests/system/Vision/fixtures/google.jpg create mode 100644 tests/system/Vision/fixtures/landmark.jpg create mode 100644 tests/system/Vision/fixtures/obama.jpg create mode 100644 tests/system/Vision/fixtures/text.jpg create mode 100644 tests/unit/Vision/Annotation/CropHintTest.php create mode 100644 tests/unit/Vision/Annotation/DocumentTest.php create mode 100644 tests/unit/Vision/Annotation/Web/WebEntityTest.php create mode 100644 tests/unit/Vision/Annotation/Web/WebImageTest.php create mode 100644 tests/unit/Vision/Annotation/Web/WebPageTest.php create mode 100644 tests/unit/Vision/Annotation/WebTest.php diff --git a/docs/toc.json b/docs/toc.json index 447db977acce..2585883aafbb 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -166,12 +166,42 @@ { "title": "Vision", "type": "vision/visionclient", + "patterns": [ + "/vision" + ], "nav": [{ "title": "Image", "type": "vision/image" }, { "title": "Annotation", - "type": "vision/annotation" + "type": "vision/annotation", + "patterns": [ + "/vision/annotation" + ], + "nav": [ + { + "title": "CropHint", + "type": "vision/annotation/crophint" + }, { + "title": "Document", + "type": "vision/annotation/document" + }, { + "title": "Entity", + "type": "vision/annotation/entity" + }, { + "title": "Face", + "type": "vision/annotation/face" + }, { + "title": "ImageProperties", + "type": "vision/annotation/imageproperties" + }, { + "title": "SafeSearch", + "type": "vision/annotation/safesearch" + }, { + "title": "Web", + "type": "vision/annotation/web" + } + ] }] } ] diff --git a/src/Vision/Annotation.php b/src/Vision/Annotation.php index 3187770600c9..321af9cbfa8c 100644 --- a/src/Vision/Annotation.php +++ b/src/Vision/Annotation.php @@ -17,10 +17,13 @@ namespace Google\Cloud\Vision; +use Google\Cloud\Vision\Annotation\CropHint; +use Google\Cloud\Vision\Annotation\Document; use Google\Cloud\Vision\Annotation\Entity; use Google\Cloud\Vision\Annotation\Face; use Google\Cloud\Vision\Annotation\ImageProperties; use Google\Cloud\Vision\Annotation\SafeSearch; +use Google\Cloud\Vision\Annotation\Web; /** * Represents a [Google Cloud Vision](https://cloud.google.com/vision) image @@ -33,7 +36,7 @@ * $cloud = new ServiceBuilder(); * $vision = $cloud->vision(); * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = $vision->image($imageResource, [ * 'FACE_DETECTION' * ]); @@ -49,42 +52,57 @@ class Annotation private $info; /** - * @var array + * @var Face[]|null */ private $faces; /** - * @var array + * @var Entity[]|null */ private $landmarks; /** - * @var array + * @var Entity[]|null */ private $logos; /** - * @var array + * @var Entity[]|null */ private $labels; /** - * @var array + * @var Entity[]|null */ private $text; /** - * @var SafeSearch + * @var Document|null + */ + private $fullText; + + /** + * @var SafeSearch|null */ private $safeSearch; /** - * @var ImageProperties + * @var ImageProperties|null */ private $imageProperties; /** - * @var array + * @var CropHint[]|null + */ + private $cropHints; + + /** + * @var Web|null + */ + private $web; + + /** + * @var array|null */ private $error; @@ -142,6 +160,10 @@ public function __construct($info) } } + if (isset($info['fullTextAnnotation'])) { + $this->fullText = new Document($info['fullTextAnnotation']); + } + if (isset($info['safeSearchAnnotation'])) { $this->safeSearch = new SafeSearch($info['safeSearchAnnotation']); } @@ -150,6 +172,17 @@ public function __construct($info) $this->imageProperties = new ImageProperties($info['imagePropertiesAnnotation']); } + if (isset($info['cropHintsAnnotation']) && is_array($info['cropHintsAnnotation']['cropHints'])) { + $this->cropHints = []; + foreach ($info['cropHintsAnnotation']['cropHints'] as $hint) { + $this->cropHints[] = new CropHint($hint); + } + } + + if (isset($info['webDetection'])) { + $this->web = new Web($info['webDetection']); + } + if (isset($info['error'])) { $this->error = $info['error']; } @@ -249,6 +282,23 @@ public function text() return $this->text; } + /** + * Return the full text annotation. + * + * Example: + * ``` + * $fullText = $annotation->fullText(); + * ``` + * + * @see https://cloud.google.com/vision/reference/rest/v1/images/annotate#fulltextannotation FullTextAnnotation + * + * @return Document|null + */ + public function fullText() + { + return $this->fullText; + } + /** * Get the result of a safe search detection * @@ -279,6 +329,36 @@ public function imageProperties() return $this->imageProperties; } + /** + * Fetch Crop Hints + * + * Example: + * ``` + * $hints = $annotation->cropHints(); + * ``` + * + * @return CropHint[]|null + */ + public function cropHints() + { + return $this->cropHints; + } + + /** + * Fetch the Web Annotatation. + * + * Example: + * ``` + * $web = $annotation->web(); + * ``` + * + * @return Web|null + */ + public function web() + { + return $this->web; + } + /** * Get error information, if present * diff --git a/src/Vision/Annotation/CropHint.php b/src/Vision/Annotation/CropHint.php new file mode 100644 index 000000000000..36c1eee94167 --- /dev/null +++ b/src/Vision/Annotation/CropHint.php @@ -0,0 +1,83 @@ +vision(); + * + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); + * $image = $vision->image($imageResource, [ 'CROP_HINTS' ]); + * $annotation = $vision->annotate($image); + * + * $hints = $annotation->cropHints(); + * $hint = $hints[0]; + * ``` + * + * @method boundingPoly() { + * The bounding polygon of the recommended crop. + * + * Example: + * ``` + * $poly = $hint->boundingPoly(); + * ``` + * + * @return array [BoundingPoly](https://cloud.google.com/vision/docs/reference/rest/v1/images/annotate#boundingpoly) + * } + * @method confidence() { + * Confidence of this being a salient region. Range [0, 1]. + * + * Example: + * ``` + * $confidence = $hint->confidence(); + * ``` + * + * @return float + * } + * @method importanceFraction() { + * Fraction of importance of this salient region with respect to the + * original image. + * + * Example: + * ``` + * $importance = $hint->importanceFraction(); + * ``` + * + * @return float + * } + */ +class CropHint extends AbstractFeature +{ + use CallTrait; + + /** + * @param array $info Crop Hint result + */ + public function __construct(array $info) + { + $this->info = $info; + } +} diff --git a/src/Vision/Annotation/Document.php b/src/Vision/Annotation/Document.php new file mode 100644 index 000000000000..48e5915a8665 --- /dev/null +++ b/src/Vision/Annotation/Document.php @@ -0,0 +1,81 @@ +vision(); + * + * $imageResource = fopen(__DIR__ . '/assets/the-constitution.jpg', 'r'); + * $image = $vision->image($imageResource, [ 'DOCUMENT_TEXT_DETECTION' ]); + * $annotation = $vision->annotate($image); + * + * $document = $annotation->fullText(); + * ``` + * + * @method pages() { + * Get the document pages. + * + * Example: + * ``` + * $pages = $document->pages(); + * ``` + * + * @return array + * } + * @method text() { + * Get the document text. + * + * Example: + * ``` + * $text = $document->text(); + * ``` + * + * @return string + * } + * @method info() { + * Get the Document Text detection result. + * + * Example: + * ``` + * $info = $document->info(); + * ``` + * + * @return array + * } + */ +class Document extends AbstractFeature +{ + use CallTrait; + + /** + * @param array $info Document Text Annotation response. + */ + public function __construct(array $info) + { + $this->info = $info; + } +} diff --git a/src/Vision/Annotation/Entity.php b/src/Vision/Annotation/Entity.php index a7a988a3046a..937ea4563498 100644 --- a/src/Vision/Annotation/Entity.php +++ b/src/Vision/Annotation/Entity.php @@ -31,7 +31,7 @@ * $cloud = new ServiceBuilder(); * $vision = $cloud->vision(); * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = $vision->image($imageResource, [ 'text' ]); * $annotation = $vision->annotate($image); * diff --git a/src/Vision/Annotation/Face.php b/src/Vision/Annotation/Face.php index eca4d39591a2..6b1369b0809f 100644 --- a/src/Vision/Annotation/Face.php +++ b/src/Vision/Annotation/Face.php @@ -30,7 +30,7 @@ * $cloud = new ServiceBuilder(); * $vision = $cloud->vision(); * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = $vision->image($imageResource, [ 'FACE_DETECTION' ]); * $annotation = $vision->annotate($image); * diff --git a/src/Vision/Annotation/Face/Landmarks.php b/src/Vision/Annotation/Face/Landmarks.php index b0688ef05ff5..5dbb3f5c8fbb 100644 --- a/src/Vision/Annotation/Face/Landmarks.php +++ b/src/Vision/Annotation/Face/Landmarks.php @@ -29,7 +29,7 @@ * $cloud = new ServiceBuilder(); * $vision = $cloud->vision(); * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = $vision->image($imageResource, ['FACE_DETECTION']); * $annotation = $vision->annotate($image); * diff --git a/src/Vision/Annotation/ImageProperties.php b/src/Vision/Annotation/ImageProperties.php index e936b6a75d3a..9b6649c91024 100644 --- a/src/Vision/Annotation/ImageProperties.php +++ b/src/Vision/Annotation/ImageProperties.php @@ -27,7 +27,7 @@ * $cloud = new ServiceBuilder(); * $vision = $cloud->vision(); * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = $vision->image($imageResource, [ 'imageProperties' ]); * $annotation = $vision->annotate($image); * diff --git a/src/Vision/Annotation/SafeSearch.php b/src/Vision/Annotation/SafeSearch.php index 46d34f86bcf4..8e3781f0f481 100644 --- a/src/Vision/Annotation/SafeSearch.php +++ b/src/Vision/Annotation/SafeSearch.php @@ -29,7 +29,7 @@ * $cloud = new ServiceBuilder(); * $vision = $cloud->vision(); * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = $vision->image($imageResource, [ 'safeSearch' ]); * $annotation = $vision->annotate($image); * diff --git a/src/Vision/Annotation/Web.php b/src/Vision/Annotation/Web.php new file mode 100644 index 000000000000..f384be49c3a1 --- /dev/null +++ b/src/Vision/Annotation/Web.php @@ -0,0 +1,170 @@ +vision(); + * + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); + * $image = $vision->image($imageResource, [ 'WEB_DETECTION' ]); + * $annotation = $vision->annotate($image); + * + * $web = $annotation->web(); + * ``` + */ +class Web extends AbstractFeature +{ + /** + * @var WebEntity[] + */ + private $entities; + + /** + * @var WebImage[]|null + */ + private $matchingImages; + + /** + * @var WebImage[]|null + */ + private $partialMatchingImages; + + /** + * @var WebPage[]|null + */ + private $pages; + + /** + * Create a Web result. + * + * @param array $info The annotation result + */ + public function __construct(array $info) + { + $this->info = $info; + + if (isset($info['webEntities'])) { + $this->entities = []; + + foreach ($info['webEntities'] as $entity) { + $this->entities[] = new WebEntity($entity); + } + } + + if (isset($info['fullMatchingImages'])) { + $this->matchingImages = []; + + foreach ($info['fullMatchingImages'] as $image) { + $this->matchingImages[] = new WebImage($image); + } + } + + if (isset($info['partialMatchingImages'])) { + $this->partialMatchingImages = []; + + foreach ($info['partialMatchingImages'] as $image) { + $this->partialMatchingImages[] = new WebImage($image); + } + } + + if (isset($info['pagesWithMatchingImages'])) { + $this->pages = []; + + foreach ($info['pagesWithMatchingImages'] as $page) { + $this->pages[] = new WebPage($page); + } + } + } + + /** + * Entities deduced from similar images on the Internet. + * + * Example: + * ``` + * $entities = $web->entities(); + * ``` + * + * @return WebEntity[]|null + */ + public function entities() + { + return $this->entities; + } + + /** + * Fully matching images from the internet. + * + * Images are most likely near duplicates, and most often are a copy of the + * given query image with a size change. + * + * Example: + * ``` + * $images = $web->matchingImages(); + * ``` + * + * @return WebImage[]|null + */ + public function matchingImages() + { + return $this->matchingImages; + } + + /** + * Partial matching images from the Internet. + * + * Those images are similar enough to share some key-point features. For + * example an original image will likely have partial matching for its crops. + * + * Example: + * ``` + * $images = $web->partialMatchingImages(); + * ``` + * + * @return WebImage[]|null + */ + public function partialMatchingImages() + { + return $this->partialMatchingImages; + } + + /** + * Web pages containing the matching images from the Internet. + * + * Example: + * ``` + * $pages = $web->pages(); + * ``` + * + * @return WebPage[]|null + */ + public function pages() + { + return $this->pages; + } +} diff --git a/src/Vision/Annotation/Web/WebEntity.php b/src/Vision/Annotation/Web/WebEntity.php new file mode 100644 index 000000000000..a6ef07e7a4ae --- /dev/null +++ b/src/Vision/Annotation/Web/WebEntity.php @@ -0,0 +1,85 @@ +vision(); + * + * $imageResource = fopen(__DIR__ . '/assets/eiffel-tower.jpg', 'r'); + * $image = $vision->image($imageResource, ['WEB_DETECTION']); + * $annotation = $vision->annotate($image); + * + * $entities = $annotation->web()->entities(); + * $firstEntity = $entities[0]; + * ``` + * + * @method entityId() { + * The Entity ID + * + * Example: + * ``` + * $id = $entity->entityId(); + * ``` + * + * @return string + * } + * @method score() { + * Overall relevancy score for the image. + * + * Not normalized and not comparable across different image queries. + * + * Example: + * ``` + * $score = $entity->score(); + * ``` + * + * @return float + * } + * @method description() { + * Canonical description of the entity, in English. + * + * Example: + * ``` + * $description = $entity->description(); + * ``` + * + * @return string + * } + */ +class WebEntity extends AbstractFeature +{ + use CallTrait; + + /** + * @param array $info WebEntity info + */ + public function __construct(array $info) + { + $this->info = $info; + } +} diff --git a/src/Vision/Annotation/Web/WebImage.php b/src/Vision/Annotation/Web/WebImage.php new file mode 100644 index 000000000000..6481628ad9be --- /dev/null +++ b/src/Vision/Annotation/Web/WebImage.php @@ -0,0 +1,75 @@ +vision(); + * + * $imageResource = fopen(__DIR__ . '/assets/eiffel-tower.jpg', 'r'); + * $image = $vision->image($imageResource, ['WEB_DETECTION']); + * $annotation = $vision->annotate($image); + * + * $matchingImages = $annotation->web()->matchingImages(); + * $firstImage = $matchingImages[0]; + * ``` + * + * @method url() { + * The result image URL + * + * Example: + * ``` + * $url = $image->url(); + * ``` + * + * @return string + * } + * @method score() { + * Overall relevancy score for the image. + * + * Not normalized and not comparable across different image queries. + * + * Example: + * ``` + * $score = $image->score(); + * ``` + * + * @return float + * } + */ +class WebImage extends AbstractFeature +{ + use CallTrait; + + /** + * @param array $info The WebImage result + */ + public function __construct(array $info) + { + $this->info = $info; + } +} diff --git a/src/Vision/Annotation/Web/WebPage.php b/src/Vision/Annotation/Web/WebPage.php new file mode 100644 index 000000000000..1d229be56fec --- /dev/null +++ b/src/Vision/Annotation/Web/WebPage.php @@ -0,0 +1,75 @@ +vision(); + * + * $imageResource = fopen(__DIR__ . '/assets/eiffel-tower.jpg', 'r'); + * $image = $vision->image($imageResource, ['WEB_DETECTION']); + * $annotation = $vision->annotate($image); + * + * $pages = $annotation->web()->pages(); + * $firstPage = $pages[0]; + * ``` + * + * @method url() { + * The result web page URL + * + * Example: + * ``` + * $url = $image->url(); + * ``` + * + * @return string + * } + * @method score() { + * Overall relevancy score for the image. + * + * Not normalized and not comparable across different image queries. + * + * Example: + * ``` + * $score = $image->score(); + * ``` + * + * @return float + * } + */ +class WebPage extends AbstractFeature +{ + use CallTrait; + + /** + * @param array $info The WebPage result + */ + public function __construct(array $info) + { + $this->info = $info; + } +} diff --git a/src/Vision/Connection/ServiceDefinition/vision-v1.json b/src/Vision/Connection/ServiceDefinition/vision-v1.json index 329a854b0671..2bcdadacfcae 100644 --- a/src/Vision/Connection/ServiceDefinition/vision-v1.json +++ b/src/Vision/Connection/ServiceDefinition/vision-v1.json @@ -44,6 +44,10 @@ "gcsImageUri": { "description": "Google Cloud Storage image URI. It must be in the following form:\n`gs://bucket_name/object_name`. For more\ndetails, please see: https://cloud.google.com/storage/docs/reference-uris.\nNOTE: Cloud Storage object versioning is not supported!", "type": "string" + }, + "ImageUri": { + "description": "Image URI which supports: 1) Google Cloud Storage image URI, which must be in the following form: `gs://bucket_name/object_name` (for details, see [Google Cloud Storage Request URIs](https://cloud.google.com/storage/docs/reference-uris)). NOTE: Cloud Storage object versioning is not supported. 2) Publicly accessible image HTTP/HTTPS URL. This is preferred over the legacy `gcs_image_uri` above. When both `gcs_image_uri` and `image_uri` are specified, `image_uri` takes precedence.", + "type": "string" } }, "id": "ImageSource" @@ -761,8 +765,11 @@ "LOGO_DETECTION", "LABEL_DETECTION", "TEXT_DETECTION", + "DOCUMENT_TEXT_DETECTION", "SAFE_SEARCH_DETECTION", - "IMAGE_PROPERTIES" + "IMAGE_PROPERTIES", + "CROP_HINTS", + "WEB_DETECTION" ], "enumDescriptions": [ "Unspecified feature type.", @@ -771,8 +778,11 @@ "Run logo detection.", "Run label detection.", "Run OCR.", + "Run dense text document OCR. Takes precedence when both DOCUMENT_TEXT_DETECTION and TEXT_DETECTION are present.", "Run various computer vision models to compute image safe-search properties.", - "Compute a set of properties about the image (such as the image's dominant colors)." + "Compute a set of properties about the image (such as the image's dominant colors).", + "Run crop hints.", + "Run web annotation." ], "type": "string" }, diff --git a/src/Vision/Image.php b/src/Vision/Image.php index 50e8f2e85404..5e3dceeaacfd 100644 --- a/src/Vision/Image.php +++ b/src/Vision/Image.php @@ -47,7 +47,7 @@ * $cloud = new ServiceBuilder(); * $vision = $cloud->vision(); * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = $vision->image($imageResource, [ * 'FACE_DETECTION' * ]); @@ -58,7 +58,7 @@ * // Images can be directly instantiated. * use Google\Cloud\Vision\Image; * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = new Image($imageResource, [ * 'FACE_DETECTION' * ]); @@ -94,7 +94,7 @@ * * use Google\Cloud\Vision\Image; * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = new Image($imageResource, [ * 'FACE_DETECTION', * 'LOGO_DETECTION' @@ -123,15 +123,18 @@ * * use Google\Cloud\Vision\Image; * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = new Image($imageResource, [ * 'faces', // Corresponds to `FACE_DETECTION` * 'landmarks', // Corresponds to `LANDMARK_DETECTION` * 'logos', // Corresponds to `LOGO_DETECTION` * 'labels', // Corresponds to `LABEL_DETECTION` - * 'text', // Corresponds to `TEXT_DETECTION` + * 'text', // Corresponds to `TEXT_DETECTION`, + * 'document', // Corresponds to `DOCUMENT_TEXT_DETECTION` * 'safeSearch', // Corresponds to `SAFE_SEARCH_DETECTION` - * 'imageProperties' // Corresponds to `IMAGE_PROPERTIES` + * 'imageProperties',// Corresponds to `IMAGE_PROPERTIES` + * 'crop', // Corresponds to `CROP_HINTS` + * 'web' // Corresponds to `WEB_DETECTION` * ]); * ``` * @@ -141,8 +144,8 @@ class Image { const TYPE_BYTES = 'bytes'; - const TYPE_STORAGE = 'storage'; const TYPE_STRING = 'string'; + const TYPE_URI = 'uri'; /** * @var mixed @@ -166,16 +169,31 @@ class Image /** * A map of short names to identifiers recognized by Cloud Vision. + * * @var array */ private $featureShortNames = [ - 'faces' => 'FACE_DETECTION', - 'landmarks' => 'LANDMARK_DETECTION', - 'logos' => 'LOGO_DETECTION', - 'labels' => 'LABEL_DETECTION', - 'text' => 'TEXT_DETECTION', - 'safeSearch' => 'SAFE_SEARCH_DETECTION', - 'imageProperties' => 'IMAGE_PROPERTIES' + 'faces' => 'FACE_DETECTION', + 'landmarks' => 'LANDMARK_DETECTION', + 'logos' => 'LOGO_DETECTION', + 'labels' => 'LABEL_DETECTION', + 'text' => 'TEXT_DETECTION', + 'document' => 'DOCUMENT_TEXT_DETECTION', + 'safeSearch' => 'SAFE_SEARCH_DETECTION', + 'imageProperties' => 'IMAGE_PROPERTIES', + 'crop' => 'CROP_HINTS', + 'web' => 'WEB_DETECTION' + ]; + + /** + * A list of allowed url schemes. + * + * @var array + */ + private $urlSchemes = [ + 'http', + 'https', + 'gs' ]; /** @@ -183,15 +201,15 @@ class Image * * @param resource|string|StorageObject $image An image to configure with * the given settings. This parameter will accept a resource, a - * string of bytes, or an instance of - * {@see Google\Cloud\Storage\StorageObject}. + * string of bytes, the URI of an image in a publicly-accessible + * web location, or an instance of {@see Google\Cloud\Storage\StorageObject}. * @param array $features A list of cloud vision * [features](https://cloud.google.com/vision/reference/rest/v1/images/annotate#type) * to apply to the image. Google Cloud Platform Client Library provides a set of abbreviated * names which can be used in the interest of brevity in place of * the names offered by the cloud vision service. These names are - * `faces`, `landmarks`, `logos`, `labels`, `text`, `safeSearch` - * and `imageProperties`. + * `faces`, `landmarks`, `logos`, `labels`, `text`, `document`, + * `safeSearch`, `imageProperties`, `crop`, and `web`. * @param array $options { * Configuration Options * @@ -218,22 +236,24 @@ public function __construct($image, array $features, array $options = []) $this->features = $this->normalizeFeatures($features); - if ($image instanceof StorageObject) { + $this->image = $image; + if (is_string($image) && in_array(parse_url($image, PHP_URL_SCHEME), $this->urlSchemes)) { + $this->type = self::TYPE_URI; + } elseif (is_string($image)) { + $this->type = self::TYPE_STRING; + } elseif ($image instanceof StorageObject) { $identity = $image->identity(); $uri = sprintf('gs://%s/%s', $identity['bucket'], $identity['object']); - $this->type = self::TYPE_STORAGE; + $this->type = self::TYPE_URI; $this->image = $uri; - } elseif (is_string($image)) { - $this->type = self::TYPE_STRING; - $this->image = $image; } elseif (is_resource($image)) { $this->type = self::TYPE_BYTES; $this->image = Psr7\stream_for($image); } else { throw new InvalidArgumentException( 'Given image is not valid. ' . - 'Image must be a string of bytes, a google storage object, or a resource.' + 'Image must be a string of bytes, a google storage object, a valid image URI, or a resource.' ); } } @@ -248,7 +268,7 @@ public function __construct($image, array $features, array $options = []) * ``` * use Google\Cloud\Vision\Image; * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * $image = new Image($imageResource, [ * 'FACE_DETECTION' * ]); @@ -302,7 +322,7 @@ private function imageObject($encode) return [ 'source' => [ - 'gcsImageUri' => $this->image + 'imageUri' => $this->image ] ]; } diff --git a/src/Vision/VisionClient.php b/src/Vision/VisionClient.php index d44aa345149c..3ee0eb437477 100644 --- a/src/Vision/VisionClient.php +++ b/src/Vision/VisionClient.php @@ -24,7 +24,7 @@ use Psr\Cache\CacheItemPoolInterface; /** - * Google Cloud Vision client allows you to understand the content of an image, + * Google Cloud Vision allows you to understand the content of an image, * classify images into categories, detect text, objects, faces and more. Find * more information at * [Google Cloud Vision docs](https://cloud.google.com/vision/docs/). @@ -110,7 +110,7 @@ public function __construct(array $config = []) * * Example: * ``` - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * * $image = $vision->image($imageResource, [ * 'FACE_DETECTION' @@ -120,7 +120,7 @@ public function __construct(array $config = []) * ``` * // Setting maxResults for a feature * - * $imageResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $imageResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * * $image = $vision->image($imageResource, [ * 'FACE_DETECTION' @@ -133,8 +133,8 @@ public function __construct(array $config = []) * * @param resource|string|StorageObject $image An image to configure with * the given settings. This parameter will accept a resource, a - * string of bytes, or an instance of - * {@see Google\Cloud\Storage\StorageObject}. + * string of bytes, the URI of an image in a publicly-accessible + * web location, or an instance of {@see Google\Cloud\Storage\StorageObject}. * @param array $features A list of cloud vision * [features](https://cloud.google.com/vision/reference/rest/v1/images/annotate#type) * to apply to the image. @@ -169,8 +169,8 @@ public function image($image, array $features, array $options = []) * // In the example below, both images will have the same settings applied. * // They will both run face detection and return up to 10 results. * - * $familyPhotoResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); - * $weddingPhotoResource = fopen(__DIR__ .'/assets/wedding-photo.jpg', 'r'); + * $familyPhotoResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); + * $weddingPhotoResource = fopen(__DIR__ . '/assets/wedding-photo.jpg', 'r'); * * $images = $vision->images([$familyPhotoResource, $weddingPhotoResource], [ * 'FACE_DETECTION' @@ -183,7 +183,8 @@ public function image($image, array $features, array $options = []) * * @param resource[]|string[]|StorageObject[] $images An array of images * to configure with the given settings. Each member of the set can - * be a resource, a string of bytes, or an instance of + * be a resource, a string of bytes, the URI of an image in a + * publicly-accessible web location, or an instance of * {@see Google\Cloud\Storage\StorageObject}. * @param array $features A list of cloud vision features to apply to each image. * @param array $options See {@see Google\Cloud\Vision\Image::__construct()} for @@ -206,7 +207,7 @@ public function images(array $images, array $features, array $options = []) * * Example: * ``` - * $familyPhotoResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); + * $familyPhotoResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); * * $image = $vision->image($familyPhotoResource, [ * 'FACE_DETECTION' @@ -232,8 +233,8 @@ public function annotate(Image $image, array $options = []) * ``` * $images = []; * - * $familyPhotoResource = fopen(__DIR__ .'/assets/family-photo.jpg', 'r'); - * $eiffelTowerResource = fopen(__DIR__ .'/assets/eiffel-tower.jpg', 'r'); + * $familyPhotoResource = fopen(__DIR__ . '/assets/family-photo.jpg', 'r'); + * $eiffelTowerResource = fopen(__DIR__ . '/assets/eiffel-tower.jpg', 'r'); * * $images[] = $vision->image($familyPhotoResource, [ * 'FACE_DETECTION' diff --git a/tests/snippets/Speech/SpeechClientTest.php b/tests/snippets/Speech/SpeechClientTest.php index b8d572eca60c..b823391e0152 100644 --- a/tests/snippets/Speech/SpeechClientTest.php +++ b/tests/snippets/Speech/SpeechClientTest.php @@ -34,7 +34,7 @@ class SpeechClientTest extends SnippetTestCase public function setUp() { - $this->testFile = "'" . __DIR__ .'/../fixtures/Speech/demo.flac' . "'"; + $this->testFile = "'" . __DIR__ . '/../fixtures/Speech/demo.flac' . "'"; $this->connection = $this->prophesize(ConnectionInterface::class); $this->client = new \SpeechClientStub; $this->client->setConnection($this->connection->reveal()); diff --git a/tests/snippets/Vision/Annotation/CropHintTest.php b/tests/snippets/Vision/Annotation/CropHintTest.php new file mode 100644 index 000000000000..7a9f60d7fae4 --- /dev/null +++ b/tests/snippets/Vision/Annotation/CropHintTest.php @@ -0,0 +1,102 @@ +info = [ + 'boundingPoly' => ['foo' => 'bar'], + 'confidence' => 0.4, + 'importanceFraction' => 0.1 + ]; + + $this->hint = new CropHint($this->info); + } + + public function testClass() + { + $connectionStub = $this->prophesize(ConnectionInterface::class); + + $connectionStub->annotate(Argument::any()) + ->willReturn([ + 'responses' => [ + [ + 'cropHintsAnnotation' => [ + 'cropHints' => [[]] + ] + ] + ] + ]); + + $snippet = $this->snippetFromClass(CropHint::class); + $snippet->addLocal('connectionStub', $connectionStub->reveal()); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); + $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); + $property = $reflection->getProperty(\'connection\'); + $property->setAccessible(true); + $property->setValue($vision, $connectionStub); + $property->setAccessible(false);' + ); + + $res = $snippet->invoke('hint'); + $this->assertInstanceOf(CropHint::class, $res->returnVal()); + } + + public function testBoundingPoly() + { + $snippet = $this->snippetFromMagicMethod(CropHint::class, 'boundingPoly'); + $snippet->addLocal('hint', $this->hint); + + $res = $snippet->invoke('poly'); + $this->assertEquals($this->info['boundingPoly'], $res->returnVal()); + } + + public function testConfidence() + { + $snippet = $this->snippetFromMagicMethod(CropHint::class, 'confidence'); + $snippet->addLocal('hint', $this->hint); + + $res = $snippet->invoke('confidence'); + $this->assertEquals($this->info['confidence'], $res->returnVal()); + } + + public function testImportanceFraction() + { + $snippet = $this->snippetFromMagicMethod(CropHint::class, 'importanceFraction'); + $snippet->addLocal('hint', $this->hint); + + $res = $snippet->invoke('importance'); + $this->assertEquals($this->info['importanceFraction'], $res->returnVal()); + } +} diff --git a/tests/snippets/Vision/Annotation/DocumentTest.php b/tests/snippets/Vision/Annotation/DocumentTest.php new file mode 100644 index 000000000000..d34f67997519 --- /dev/null +++ b/tests/snippets/Vision/Annotation/DocumentTest.php @@ -0,0 +1,98 @@ +info = [ + 'pages' => [['foo' => 'bar']], + 'text' => 'hello world' + ]; + $this->document = new Document($this->info); + } + + public function testClass() + { + $connectionStub = $this->prophesize(ConnectionInterface::class); + + $connectionStub->annotate(Argument::any()) + ->willReturn([ + 'responses' => [ + [ + 'fullTextAnnotation' => [[]] + ] + ] + ]); + + $snippet = $this->snippetFromClass(Document::class); + $snippet->addLocal('connectionStub', $connectionStub->reveal()); + $snippet->replace( + "__DIR__ . '/assets/the-constitution.jpg'", + "'php://temp'" + ); + $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); + $property = $reflection->getProperty(\'connection\'); + $property->setAccessible(true); + $property->setValue($vision, $connectionStub); + $property->setAccessible(false);' + ); + + $res = $snippet->invoke('document'); + $this->assertInstanceOf(Document::class, $res->returnVal()); + } + + public function testPages() + { + $snippet = $this->snippetFromMagicMethod(Document::class, 'pages'); + $snippet->addLocal('document', $this->document); + + $res = $snippet->invoke('pages'); + $this->assertEquals($this->info['pages'], $res->returnVal()); + } + + public function testText() + { + $snippet = $this->snippetFromMagicMethod(Document::class, 'text'); + $snippet->addLocal('document', $this->document); + + $res = $snippet->invoke('text'); + $this->assertEquals($this->info['text'], $res->returnVal()); + } + + public function testInfo() + { + $snippet = $this->snippetFromMagicMethod(Document::class, 'info'); + $snippet->addLocal('document', $this->document); + + $res = $snippet->invoke('info'); + $this->assertEquals($this->info, $res->returnVal()); + } +} diff --git a/tests/snippets/Vision/Annotation/EntityTest.php b/tests/snippets/Vision/Annotation/EntityTest.php index b19d7b2268dd..d990ee8c27ce 100644 --- a/tests/snippets/Vision/Annotation/EntityTest.php +++ b/tests/snippets/Vision/Annotation/EntityTest.php @@ -62,7 +62,10 @@ public function testClass() $snippet = $this->snippetFromClass(Entity::class); $snippet->addLocal('connectionStub', $connectionStub->reveal()); - $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); $property = $reflection->getProperty(\'connection\'); $property->setAccessible(true); @@ -161,5 +164,4 @@ public function testProperties() $res = $snippet->invoke(); $this->assertEquals($this->entityData['properties'], $res->output()); } - } diff --git a/tests/snippets/Vision/Annotation/Face/LandmarksTest.php b/tests/snippets/Vision/Annotation/Face/LandmarksTest.php index 9dc833cf7993..858a6a810f6e 100644 --- a/tests/snippets/Vision/Annotation/Face/LandmarksTest.php +++ b/tests/snippets/Vision/Annotation/Face/LandmarksTest.php @@ -98,7 +98,10 @@ public function testClass() $snippet = $this->snippetFromClass(Landmarks::class); $snippet->addLocal('connectionStub', $connectionStub->reveal()); - $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); $property = $reflection->getProperty(\'connection\'); $property->setAccessible(true); diff --git a/tests/snippets/Vision/Annotation/FaceTest.php b/tests/snippets/Vision/Annotation/FaceTest.php index 58dec319541c..81a9127a5a83 100644 --- a/tests/snippets/Vision/Annotation/FaceTest.php +++ b/tests/snippets/Vision/Annotation/FaceTest.php @@ -74,7 +74,10 @@ public function testClass() $snippet = $this->snippetFromClass(Face::class); $snippet->addLocal('connectionStub', $connectionStub->reveal()); - $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); $property = $reflection->getProperty(\'connection\'); $property->setAccessible(true); diff --git a/tests/snippets/Vision/Annotation/ImagePropertiesTest.php b/tests/snippets/Vision/Annotation/ImagePropertiesTest.php index 03ea867f8f86..62c8c8f82f64 100644 --- a/tests/snippets/Vision/Annotation/ImagePropertiesTest.php +++ b/tests/snippets/Vision/Annotation/ImagePropertiesTest.php @@ -53,7 +53,10 @@ public function testClass() $snippet = $this->snippetFromClass(ImageProperties::class); $snippet->addLocal('connectionStub', $connectionStub->reveal()); - $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); $property = $reflection->getProperty(\'connection\'); $property->setAccessible(true); diff --git a/tests/snippets/Vision/Annotation/SafeSearchTest.php b/tests/snippets/Vision/Annotation/SafeSearchTest.php index e9695f1dde87..b0af9e9a5c30 100644 --- a/tests/snippets/Vision/Annotation/SafeSearchTest.php +++ b/tests/snippets/Vision/Annotation/SafeSearchTest.php @@ -58,7 +58,10 @@ public function testClass() $snippet = $this->snippetFromClass(SafeSearch::class); $snippet->addLocal('connectionStub', $connectionStub->reveal()); - $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); $property = $reflection->getProperty(\'connection\'); $property->setAccessible(true); @@ -70,14 +73,6 @@ public function testClass() $this->assertInstanceOf(SafeSearch::class, $res->returnVal()); } - public function testInfo() - { - $snippet = $this->snippetFromMagicMethod(SafeSearch::class, 'info'); - $snippet->addLocal('safeSearch', $this->ss); - - $this->assertEquals($this->ssData, $snippet->invoke('info')->returnVal()); - } - public function testAdult() { $snippet = $this->snippetFromMagicMethod(SafeSearch::class, 'adult'); @@ -122,6 +117,7 @@ public function testIsAdult() $res = $snippet->invoke(); $this->assertEquals(sprintf('Image contains %s content.', 'adult'), $res->output()); } + public function testIsSpoof() { $snippet = $this->snippetFromMethod(SafeSearch::class, 'isSpoof'); @@ -130,6 +126,7 @@ public function testIsSpoof() $res = $snippet->invoke(); $this->assertEquals(sprintf('Image contains %s content.', 'spoofed'), $res->output()); } + public function testIsMedical() { $snippet = $this->snippetFromMethod(SafeSearch::class, 'isMedical'); @@ -138,6 +135,7 @@ public function testIsMedical() $res = $snippet->invoke(); $this->assertEquals(sprintf('Image contains %s content.', 'medical'), $res->output()); } + public function testIsViolent() { $snippet = $this->snippetFromMethod(SafeSearch::class, 'isViolent'); @@ -146,4 +144,13 @@ public function testIsViolent() $res = $snippet->invoke(); $this->assertEquals(sprintf('Image contains %s content.', 'violent'), $res->output()); } + + public function testInfo() + { + $snippet = $this->snippetFromMagicMethod(SafeSearch::class, 'info'); + $snippet->addLocal('safeSearch', $this->ss); + + $res = $snippet->invoke('info'); + $this->assertEquals($this->ssData, $res->returnVal()); + } } diff --git a/tests/snippets/Vision/Annotation/Web/WebEntityTest.php b/tests/snippets/Vision/Annotation/Web/WebEntityTest.php new file mode 100644 index 000000000000..62009e184dcb --- /dev/null +++ b/tests/snippets/Vision/Annotation/Web/WebEntityTest.php @@ -0,0 +1,103 @@ +info = [ + 'entityId' => 'foo', + 'score' => 0.1, + 'description' => 'bar' + ]; + $this->entity = new WebEntity($this->info); + } + + public function testClass() + { + $connectionStub = $this->prophesize(ConnectionInterface::class); + + $connectionStub->annotate(Argument::any()) + ->willReturn([ + 'responses' => [ + [ + 'webDetection' => [ + 'webEntities' => [ + [] + ] + ] + ] + ] + ]); + + $snippet = $this->snippetFromClass(WebEntity::class); + $snippet->addLocal('connectionStub', $connectionStub->reveal()); + $snippet->replace( + "__DIR__ . '/assets/eiffel-tower.jpg'", + "'php://temp'" + ); + $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); + $property = $reflection->getProperty(\'connection\'); + $property->setAccessible(true); + $property->setValue($vision, $connectionStub); + $property->setAccessible(false);' + ); + + $res = $snippet->invoke('firstEntity'); + $this->assertInstanceOf(WebEntity::class, $res->returnVal()); + } + + public function testEntityId() + { + $snippet = $this->snippetFromMagicMethod(WebEntity::class, 'entityId'); + $snippet->addLocal('entity', $this->entity); + + $res = $snippet->invoke('id'); + $this->assertEquals($this->info['entityId'], $res->returnVal()); + } + + public function testScore() + { + $snippet = $this->snippetFromMagicMethod(WebEntity::class, 'score'); + $snippet->addLocal('entity', $this->entity); + + $res = $snippet->invoke('score'); + $this->assertEquals($this->info['score'], $res->returnVal()); + } + + public function testDescription() + { + $snippet = $this->snippetFromMagicMethod(WebEntity::class, 'description'); + $snippet->addLocal('entity', $this->entity); + + $res = $snippet->invoke('description'); + $this->assertEquals($this->info['description'], $res->returnVal()); + } +} diff --git a/tests/snippets/Vision/Annotation/Web/WebImageTest.php b/tests/snippets/Vision/Annotation/Web/WebImageTest.php new file mode 100644 index 000000000000..b8ced484b27c --- /dev/null +++ b/tests/snippets/Vision/Annotation/Web/WebImageTest.php @@ -0,0 +1,93 @@ +info = [ + 'url' => 'http://foo.bar/image.jpg', + 'score' => 0.1, + ]; + $this->image = new WebImage($this->info); + } + + public function testClass() + { + $connectionStub = $this->prophesize(ConnectionInterface::class); + + $connectionStub->annotate(Argument::any()) + ->willReturn([ + 'responses' => [ + [ + 'webDetection' => [ + 'fullMatchingImages' => [ + [] + ] + ] + ] + ] + ]); + + $snippet = $this->snippetFromClass(WebImage::class); + $snippet->addLocal('connectionStub', $connectionStub->reveal()); + $snippet->replace( + "__DIR__ . '/assets/eiffel-tower.jpg'", + "'php://temp'" + ); + $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); + $property = $reflection->getProperty(\'connection\'); + $property->setAccessible(true); + $property->setValue($vision, $connectionStub); + $property->setAccessible(false);' + ); + + $res = $snippet->invoke('firstImage'); + $this->assertInstanceOf(WebImage::class, $res->returnVal()); + } + + public function testurl() + { + $snippet = $this->snippetFromMagicMethod(WebImage::class, 'url'); + $snippet->addLocal('image', $this->image); + + $res = $snippet->invoke('url'); + $this->assertEquals($this->info['url'], $this->image->url()); + } + + public function testscore() + { + $snippet = $this->snippetFromMagicMethod(WebImage::class, 'score'); + $snippet->addLocal('image', $this->image); + + $res = $snippet->invoke('score'); + $this->assertEquals($this->info['score'], $this->image->score()); + } +} diff --git a/tests/snippets/Vision/Annotation/Web/WebPageTest.php b/tests/snippets/Vision/Annotation/Web/WebPageTest.php new file mode 100644 index 000000000000..5e74eafcbe7e --- /dev/null +++ b/tests/snippets/Vision/Annotation/Web/WebPageTest.php @@ -0,0 +1,93 @@ +info = [ + 'url' => 'http://foo.bar/image.jpg', + 'score' => 0.1, + ]; + $this->image = new WebPage($this->info); + } + + public function testClass() + { + $connectionStub = $this->prophesize(ConnectionInterface::class); + + $connectionStub->annotate(Argument::any()) + ->willReturn([ + 'responses' => [ + [ + 'webDetection' => [ + 'pagesWithMatchingImages' => [ + [] + ] + ] + ] + ] + ]); + + $snippet = $this->snippetFromClass(WebPage::class); + $snippet->addLocal('connectionStub', $connectionStub->reveal()); + $snippet->replace( + "__DIR__ . '/assets/eiffel-tower.jpg'", + "'php://temp'" + ); + $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); + $property = $reflection->getProperty(\'connection\'); + $property->setAccessible(true); + $property->setValue($vision, $connectionStub); + $property->setAccessible(false);' + ); + + $res = $snippet->invoke('firstPage'); + $this->assertInstanceOf(WebPage::class, $res->returnVal()); + } + + public function testurl() + { + $snippet = $this->snippetFromMagicMethod(WebPage::class, 'url'); + $snippet->addLocal('image', $this->image); + + $res = $snippet->invoke('url'); + $this->assertEquals($this->info['url'], $this->image->url()); + } + + public function testscore() + { + $snippet = $this->snippetFromMagicMethod(WebPage::class, 'score'); + $snippet->addLocal('image', $this->image); + + $res = $snippet->invoke('score'); + $this->assertEquals($this->info['score'], $this->image->score()); + } +} diff --git a/tests/snippets/Vision/Annotation/WebTest.php b/tests/snippets/Vision/Annotation/WebTest.php new file mode 100644 index 000000000000..38f849b2a8c3 --- /dev/null +++ b/tests/snippets/Vision/Annotation/WebTest.php @@ -0,0 +1,120 @@ +info = [ + 'webEntities' => [ + [] + ], + 'fullMatchingImages' => [ + [] + ], + 'partialMatchingImages' => [ + [] + ], + 'pagesWithMatchingImages' => [ + [] + ] + ]; + $this->web = new Web($this->info); + } + + public function testClass() + { + $connectionStub = $this->prophesize(ConnectionInterface::class); + + $connectionStub->annotate(Argument::any()) + ->willReturn([ + 'responses' => [ + [ + 'webDetection' => [] + ] + ] + ]); + + $snippet = $this->snippetFromClass(Web::class); + $snippet->addLocal('connectionStub', $connectionStub->reveal()); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); + $snippet->insertAfterLine(3, '$reflection = new \ReflectionClass($vision); + $property = $reflection->getProperty(\'connection\'); + $property->setAccessible(true); + $property->setValue($vision, $connectionStub); + $property->setAccessible(false);' + ); + + $res = $snippet->invoke('web'); + $this->assertInstanceOf(Web::class, $res->returnVal()); + } + + public function testEntities() + { + $snippet = $this->snippetFromMethod(Web::class, 'entities'); + $snippet->addLocal('web', $this->web); + + $res = $snippet->invoke('entities'); + $this->assertInstanceOf(WebEntity::class, $res->returnVal()[0]); + } + + public function testMatchingImages() + { + $snippet = $this->snippetFromMethod(Web::class, 'matchingImages'); + $snippet->addLocal('web', $this->web); + + $res = $snippet->invoke('images'); + $this->assertInstanceOf(WebImage::class, $res->returnVal()[0]); + } + + public function testPartialMatchingImages() + { + $snippet = $this->snippetFromMethod(Web::class, 'partialMatchingImages'); + $snippet->addLocal('web', $this->web); + + $res = $snippet->invoke('images'); + $this->assertInstanceOf(WebImage::class, $res->returnVal()[0]); + } + + public function testPages() + { + $snippet = $this->snippetFromMethod(Web::class, 'pages'); + $snippet->addLocal('web', $this->web); + + $res = $snippet->invoke('pages'); + $this->assertInstanceOf(WebPage::class, $res->returnVal()[0]); + } +} diff --git a/tests/snippets/Vision/AnnotationTest.php b/tests/snippets/Vision/AnnotationTest.php index 6412667a4dca..16f9dd158454 100644 --- a/tests/snippets/Vision/AnnotationTest.php +++ b/tests/snippets/Vision/AnnotationTest.php @@ -19,10 +19,13 @@ use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Vision\Annotation; +use Google\Cloud\Vision\Annotation\CropHint; +use Google\Cloud\Vision\Annotation\Document; use Google\Cloud\Vision\Annotation\Entity; use Google\Cloud\Vision\Annotation\Face; use Google\Cloud\Vision\Annotation\ImageProperties; use Google\Cloud\Vision\Annotation\SafeSearch; +use Google\Cloud\Vision\Annotation\Web; use Google\Cloud\Vision\Connection\ConnectionInterface; use Prophecy\Argument; @@ -35,8 +38,8 @@ public function testClass() { $snippet = $this->snippetFromClass(Annotation::class); $snippet->replace( - '__DIR__ .\'/assets/family-photo.jpg\'', - '\'php://temp\'' + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" ); $connectionStub = $this->prophesize(ConnectionInterface::class); @@ -151,6 +154,42 @@ public function testImageProperties() $this->assertInstanceOf(ImageProperties::class, $res->returnVal()); } + public function testFullText() + { + $ft = ['foo' => 'bar']; + $snippet = $this->snippetFromMethod(Annotation::class, 'fullText'); + $snippet->addLocal('annotation', new Annotation([ + 'fullTextAnnotation' => $ft + ])); + + $res = $snippet->invoke('fullText'); + $this->assertInstanceOf(Document::class, $res->returnVal()); + } + + public function testCropHints() + { + $snippet = $this->snippetFromMethod(Annotation::class, 'cropHints'); + $snippet->addLocal('annotation', new Annotation([ + 'cropHintsAnnotation' => [ + 'cropHints' => [[]] + ] + ])); + + $res = $snippet->invoke('hints'); + $this->assertInstanceOf(CropHint::class, $res->returnVal()[0]); + } + + public function testWeb() + { + $snippet = $this->snippetFromMethod(Annotation::class, 'web'); + $snippet->addLocal('annotation', new Annotation([ + 'webDetection' => [] + ])); + + $res = $snippet->invoke('web'); + $this->assertInstanceOf(Web::class, $res->returnVal()); + } + public function testError() { $snippet = $this->snippetFromMethod(Annotation::class, 'error'); diff --git a/tests/snippets/Vision/ImageTest.php b/tests/snippets/Vision/ImageTest.php index 3d2dac623442..5473e64df24a 100644 --- a/tests/snippets/Vision/ImageTest.php +++ b/tests/snippets/Vision/ImageTest.php @@ -29,7 +29,10 @@ class ImageTest extends SnippetTestCase public function testImageFromServiceBuilder() { $snippet = $this->snippetFromClass(Image::class, 'default'); - $snippet->setLine(6, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $res = $snippet->invoke('image'); $this->assertInstanceOf(Image::class, $res->returnVal()); @@ -38,7 +41,10 @@ public function testImageFromServiceBuilder() public function testDirectInstantiation() { $snippet = $this->snippetFromClass(Image::class, 'direct'); - $snippet->setLine(4, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $res = $snippet->invoke('image'); $this->assertInstanceOf(Image::class, $res->returnVal()); @@ -76,7 +82,10 @@ public function testMaxResults() public function testFeatureShortcuts() { $snippet = $this->snippetFromClass(Image::class, 'shortcut'); - $snippet->setLine(5, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $res = $snippet->invoke('image'); $this->assertInstanceOf(Image::class, $res->returnVal()); @@ -85,7 +94,10 @@ public function testFeatureShortcuts() public function testRequestObject() { $snippet = $this->snippetFromMethod(Image::class, 'requestObject'); - $snippet->setLine(2, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $res = $snippet->invoke('requestObj'); $this->assertTrue(array_key_exists('image', $res->returnVal())); diff --git a/tests/snippets/Vision/VisionClientTest.php b/tests/snippets/Vision/VisionClientTest.php index 9d543b2fc957..4b7d677541c7 100644 --- a/tests/snippets/Vision/VisionClientTest.php +++ b/tests/snippets/Vision/VisionClientTest.php @@ -60,7 +60,10 @@ public function testImage() $snippet = $this->snippetFromMethod(VisionClient::class, 'image'); $snippet->addLocal('vision', $this->client); - $snippet->setLine(0, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $res = $snippet->invoke('image'); @@ -72,7 +75,10 @@ public function testImageWithMaxResults() $snippet = $this->snippetFromMethod(VisionClient::class, 'image', 1); $snippet->addLocal('vision', $this->client); - $snippet->setLine(2, '$imageResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $res = $snippet->invoke('image'); @@ -84,8 +90,15 @@ public function testImages() $snippet = $this->snippetFromMethod(VisionClient::class, 'images'); $snippet->addLocal('vision', $this->client); - $snippet->setLine(3, '$familyPhotoResource = fopen(\'php://temp\', \'r\');'); - $snippet->setLine(4, '$weddingPhotoResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); + + $snippet->replace( + "__DIR__ . '/assets/wedding-photo.jpg'", + "'php://temp'" + ); $res = $snippet->invoke('images'); $this->assertInstanceOf(Image::class, $res->returnVal()[0]); @@ -97,7 +110,10 @@ public function testAnnotate() $snippet = $this->snippetFromMethod(VisionClient::class, 'annotate'); $snippet->addLocal('vision', $this->client); - $snippet->setLine(0, '$familyPhotoResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); $this->connection->annotate(Argument::any()) ->shouldBeCalled() @@ -119,8 +135,15 @@ public function testAnnotateBatch() $snippet = $this->snippetFromMethod(VisionClient::class, 'annotateBatch'); $snippet->addLocal('vision', $this->client); - $snippet->setLine(2, '$familyPhotoResource = fopen(\'php://temp\', \'r\');'); - $snippet->setLine(3, '$eiffelTowerResource = fopen(\'php://temp\', \'r\');'); + $snippet->replace( + "__DIR__ . '/assets/family-photo.jpg'", + "'php://temp'" + ); + + $snippet->replace( + "__DIR__ . '/assets/eiffel-tower.jpg'", + "'php://temp'" + ); $this->connection->annotate(Argument::any()) ->shouldBeCalled() diff --git a/tests/snippets/bootstrap.php b/tests/snippets/bootstrap.php index 1b6d6c687a58..d6448ffe6885 100644 --- a/tests/snippets/bootstrap.php +++ b/tests/snippets/bootstrap.php @@ -2,7 +2,7 @@ // Provide a project ID. If you're mocking your service calls (and if you aren't // start now) you don't need anything else. -putenv('GOOGLE_APPLICATION_CREDENTIALS='. __DIR__ .'/keyfile-stub.json'); +putenv('GOOGLE_APPLICATION_CREDENTIALS='. __DIR__ . '/keyfile-stub.json'); use Google\Cloud\Dev\Snippet\Container; use Google\Cloud\Dev\Snippet\Coverage\Coverage; @@ -12,7 +12,7 @@ require __DIR__ . '/../../vendor/autoload.php'; $parser = new Parser; -$scanner = new Scanner($parser, __DIR__ .'/../../src'); +$scanner = new Scanner($parser, __DIR__ . '/../../src'); $coverage = new Coverage($scanner); $coverage->buildListToCover(); @@ -22,11 +22,11 @@ register_shutdown_function(function () { $uncovered = Container::$coverage->uncovered(); - if (!file_exists(__DIR__ .'/../../build')) { - mkdir(__DIR__ .'/../../build', 0777, true); + if (!file_exists(__DIR__ . '/../../build')) { + mkdir(__DIR__ . '/../../build', 0777, true); } - file_put_contents(__DIR__ .'/../../build/snippets-uncovered.json', json_encode($uncovered, JSON_PRETTY_PRINT)); + file_put_contents(__DIR__ . '/../../build/snippets-uncovered.json', json_encode($uncovered, JSON_PRETTY_PRINT)); if (!empty($uncovered)) { echo sprintf("\033[31mNOTICE: %s uncovered snippets! See build/snippets-uncovered.json for a report.\n", count($uncovered)); diff --git a/tests/system/Vision/AnnotationsTest.php b/tests/system/Vision/AnnotationsTest.php new file mode 100644 index 000000000000..cd160140ade6 --- /dev/null +++ b/tests/system/Vision/AnnotationsTest.php @@ -0,0 +1,169 @@ +client = parent::$vision; + } + + public function testAnnotate() + { + $image = $this->client->image( + file_get_contents($this->getFixtureFilePath('landmark.jpg')) , [ + 'LANDMARK_DETECTION', + 'SAFE_SEARCH_DETECTION', + 'IMAGE_PROPERTIES', + 'CROP_HINTS', + 'WEB_DETECTION' + ] + ); + + $res = $this->client->annotate($image); + $this->assertInstanceOf(Annotation::class, $res); + + // Landmarks + $this->assertInstanceOf(Entity::class, $res->landmarks()[0]); + $this->assertEquals('Eiffel Tower', $res->landmarks()[0]->description()); + + // Safe Search + $this->assertInstanceOf(SafeSearch::class, $res->safeSearch()); + $this->assertEquals('VERY_UNLIKELY', $res->safeSearch()->adult()); + $this->assertEquals('VERY_UNLIKELY', $res->safeSearch()->spoof()); + $this->assertEquals('VERY_UNLIKELY', $res->safeSearch()->medical()); + $this->assertEquals('VERY_UNLIKELY', $res->safeSearch()->violence()); + $this->assertFalse($res->safeSearch()->isAdult()); + $this->assertFalse($res->safeSearch()->isSpoof()); + $this->assertFalse($res->safeSearch()->isMedical()); + $this->assertFalse($res->safeSearch()->isViolent()); + + // Image Properties + $this->assertInstanceOf(ImageProperties::class, $res->imageProperties()); + $this->assertTrue(is_array($res->imageProperties()->colors())); + + // Crop Hints + $this->assertInstanceOf(CropHint::class, $res->cropHints()[0]); + $this->assertTrue(isset($res->cropHints()[0]->boundingPoly()['vertices'])); + $this->assertTrue(is_float($res->cropHints()[0]->confidence())); + $this->assertTrue(!is_null($res->cropHints()[0]->importanceFraction())); + + // Web Annotation + $this->assertInstanceOf(Web::class, $res->web()); + $this->assertInstanceOf(WebEntity::class, $res->web()->entities()[0]); + + $desc = array_filter($res->web()->entities(), function ($e) { + return ($e->description() === 'Eiffel Tower'); + }); + $this->assertTrue(count($desc) > 0); + + $this->assertInstanceOf(WebImage::class, $res->web()->matchingImages()[0]); + $this->assertInstanceOf(WebImage::class, $res->web()->partialMatchingImages()[0]); + $this->assertInstanceOf(WebPage::class, $res->web()->pages()[0]); + } + + public function testFaceAndLabelDetection() + { + $image = $this->client->image( + file_get_contents($this->getFixtureFilePath('obama.jpg')) , [ + 'FACE_DETECTION', + 'LABEL_DETECTION' + ] + ); + + $res = $this->client->annotate($image); + + $this->assertInstanceOf(Annotation::class, $res); + + // Face Detection + $this->assertInstanceOf(Face::class, $res->faces()[0]); + $this->assertInstanceOf(Landmarks::class, $res->faces()[0]->landmarks()); + $this->assertTrue($res->faces()[0]->isJoyful()); + $this->assertFalse($res->faces()[0]->isSorrowful()); + $this->assertFalse($res->faces()[0]->isAngry()); + $this->assertFalse($res->faces()[0]->isSurprised()); + $this->assertFalse($res->faces()[0]->isUnderExposed()); + $this->assertFalse($res->faces()[0]->isBlurred()); + $this->assertFalse($res->faces()[0]->hasHeadwear()); + + // Label Detection + $this->assertInstanceOf(Entity::class, $res->labels()[0]); + } + + public function testLogoDetection() + { + $image = $this->client->image( + file_get_contents($this->getFixtureFilePath('google.jpg')) , [ + 'LOGO_DETECTION' + ] + ); + + $res = $this->client->annotate($image); + $this->assertInstanceOf(Annotation::class, $res); + $this->assertInstanceOf(Entity::class, $res->logos()[0]); + $this->assertEquals('Google', $res->logos()[0]->description()); + } + + public function testTextDetection() + { + $image = $this->client->image( + file_get_contents($this->getFixtureFilePath('text.jpg')) , [ + 'TEXT_DETECTION' + ] + ); + + $res = $this->client->annotate($image); + $this->assertInstanceOf(Annotation::class, $res); + $this->assertInstanceOf(Entity::class, $res->text()[0]); + $this->assertEquals("Hello World.", explode("\n", $res->text()[0]->description())[0]); + $this->assertEquals("Goodby World!", explode("\n", $res->text()[0]->description())[1]); + } + + public function testDocumentTextDetection() + { + $image = $this->client->image( + file_get_contents($this->getFixtureFilePath('text.jpg')) , [ + 'DOCUMENT_TEXT_DETECTION' + ] + ); + + $res = $this->client->annotate($image); + + $this->assertInstanceOf(Annotation::class, $res); + $this->assertInstanceOf(Document::class, $res->fullText()); + } +} diff --git a/tests/system/Vision/VisionTestCase.php b/tests/system/Vision/VisionTestCase.php new file mode 100644 index 000000000000..40fca44dc242 --- /dev/null +++ b/tests/system/Vision/VisionTestCase.php @@ -0,0 +1,47 @@ + $keyFilePath + ]); + self::$hasSetUp = true; + } + + protected function getFixtureFilePath($file) + { + return __DIR__ .'/fixtures/'. $file; + } +} diff --git a/tests/system/Vision/fixtures/google.jpg b/tests/system/Vision/fixtures/google.jpg new file mode 100644 index 0000000000000000000000000000000000000000..29834cfede17d011d85b55c3b98d43e4e7ebd386 GIT binary patch literal 3098 zcmbuCcTiL58pclu2~Bzt>0Lk&2z5ca6zPPfR4LM=lh9P82_g~*MT)dQ=)D9KkfJov zY#=~@AmAc`g186>0?TrP?#|r5?mzea&Y3wg=gc#|d7m@y^L?jtr|$t46T@qU00;yC zAj$$x7l0%H{IjSj1vx7k8Yq;8hJlujvKUwy&M{CfR%SM4Cd$pr$a zXn(!fn+ER1#sUD7`e!%9-JDi`)ewN`0oB7-9(%JCF|>o(@L>KnXBPhTO+X=MgL;q2 z3IG_PjB^2Z^lJ6YJIVl*yUSXn0fO30bqL@|kDUyr@nN`7rl$4M`8{yg%D8;M`ET#i z^(E-8c^q)nRx#~%Zm2bly-okJ4j>}!R-fAa`IZjAr{G=iE;~}5LJFp^^HJc=(Ep1a z0;Q&*1yO;2qEoR@KBVTNewzh>Tlw9KAzXXHiic61?QGpR0x~Mj?%RM9zK(a z^WjyHXv6v3aF!s<>+9TPcDoS~vG1BUTeTHYg9}yU|3QWdFLyg8lrO1QLC>hDSEpOzh1bxNon0^zT$Ne) zBl!MOBMDDV`pn{oy6-o33f#Nc)ym6`*XjA5MzHlRFnRPGRx+2f0R%h#K9F=cL|)$s z??H?>#eTcejcsVRp|$o`w`6p=6`AeZ1zpwmRT+MV#fU~GsNCD2S7~&I@;mUtT``8J z_SR2{gCuRk=C{0V0+wAR8Jh7r#CrXlZ9lQqlJf^Onm3hd%FsmsdDyZEBtGF<0mC}2y={2`ISmTN&UPzpE@ z;|zE4SCYoBRF~T@_U1Q7ndl|QF!(gke&1aQjHr%iGJw~bBV|o8CJ?4@ssUoClzMK> zhRV0C)cdK1`zn20|NH5sjO~lAx*zl|9!_%dUsOq%Pgv%eRM9*{wR>9PWrb$SL2S5F z0M4u;@GQqw344N#glp^&Pl2Ypw@(3?`WKV^>z4R|I16zb0&P3p*H470<%iz4-sP4n z+Cb!(KdYaN3}LCR0gPV7q)2va{U+U*{J0}gW4+SIu!-zhJx$?y4ZWvqm5ixqe$&=f zmAo6*=dhA~V0@G$$0orz+oT3FJdCcWVriRK7n8hgf%jMvYFYkqZE5afudaLlcJ_x-xcEiG|qz%5-Sc=v%6=K)_+To%!^h6^a zLn4uO`VZ|FJcQ|q$Q-Hy zx4UBH>?W-b1*-NUUgOR9if$R+f{~3-sGN2FFc8edKJDkoX2r=bL^{X8A-NjJBcv$DQH8ImsrwT3L--MeQ{qVNjBzk zAVoKg^C6Qw-RrJVmpn}x_L_sh!>0pFNTvm68MaxJI2N`IODUdN;Jk%JI!jPqG*9Do%Tb?Pot}$CV@i_Dqa3?Ot0q_PO(Gg zL6k$BHOy2^MfZ?spJ?U9u!e<3JC+iNtlV-|Aai!{xVtXgd*)Wed0t<^iK4B@?VXQ{ zNE)deU;GsntyW^s(Ja=eUT)8lGS}5u0V)uf3IwHN{3kMg&UID*D$K?%PZ{hqoFXuI z0(y|h!f{?mN6*IHBR&f=`75PBXM-H)l)Pv*tgqCd2|{m8EQa``YkcTsK^jDrHDAeJ zeRae*u;;M(iXaMqIhV@Bm$)vt?VmjB5OT4=QViJ>VqHbfb%(VN;ZaTMm=+S=9&YDr z4`a}NiGTw9ugVr3E_MXIDfZ{PqSVGs2uDBAnrh5`_SIFf0@vyDs0xv)`?H2^Tg@r(^e>*KIfwdteBa@( zP2k*bS{wr2hpLUE67RvUId#a%kHD6dA_S`QzLEwjnxBR*)Jb73=in5sDI%rIf_~GM zAS`zFFm^inV~cvGmEy`1 zY|(om$fY+IJ~a)tbWvCx7E8}T?s1$pvA!dJGY`5uBhXpSRji~fc9>$g%9-I*UAY!U9pAzlNFz*d{ zG5Ul31tT}{oz5L;`>#`TM{21({SEnJKp!67x!IvaHVv*b{#)KIV=6-Kr2KlVOuZ%t znq&%ZXw{!W!)lj+&jhA5BOf8Ew-%3-z^;jTp#fEnJr9&*l$oUy{}@`yhT**`b1R|O zMbh&nx>?IsLqnSDy6p)!?1ZDe!`|MX_(UU|mcEaghYiC_Ze*&~fp`Sj6b`!QkCB2i z_cuA_OP1}fgp`}{7}7HU&}zc7w(*w5TK;D-R?H<|ypOl4Ui;+FPSd`C!LAm$%%OA; z>v8&kE;Vb7X#Vig=#8*?)O9m=d!cII%&6=~?_N0Rn(e;b{&Km9d$a!ymoM40k(;l| z<|Og}K)<>$6GU|Ns%dFR-68Nh`1$^x zd!PH?9p>Q~*m3q=`(5vP*E(mO=ATwUFXW}=q(N|Sa3ERW2lVs^GL^Qmx1e%$bayec z5Tvqpb8~VPeEZhS5e#;8aAotfv9keNnA@1JIl5TAeg5n1bALf~pudrnjisf9y^+@c z?PK)rzfndEW-g9SP8Q~jY_?8TPcYCs5HcbXG7=&(G7>Th3NkAC3v_feG<1Ay9LyK5 z@QH|C;S&;)kke6;kkXJ55>l~H(Y&E&WMU+yWMyY%V5ehXWO$wg4h01T9Toj0I{Hfn z5<(J&|KG2t4iGLX+!xRZ0vs*~9v2P)7w)MCLoeo`&@w-v&g47Xm(BU-T#NS8ZN8-cxf@;Kx za;8%iLq4qu8Cm7@(TFjyQP>AR9mzec;A z1*-Nt+7)kJ5RE~97Uk9xh@eP#3$^H_2SeY-d`);FWTWJms3lwU7Yy)W=>M-x0V_8R zdY%hO+`JdGi}K&*@EOE&2+)a25v`HbgQGzBC~|+r#P1$DXsB!&EB@y46p8H_&Ybg_ zJ{azt349IgjDN^`WOn6A1Z)7l^;1qO~g*PCvBkGB>+QeKLgsKqG}ZO)nGsKWts6@$i+l$ihv zn+7q!|C=Nzl>tRu5ST59Lmm0KMFUP9Nf{qbJybdWc1gOeT!Np{Wbj_|WoiV}gY%QS zoRqsDE9QKs>$=`=9r?ISIqy{Jd`AA)Cqqj1F9gKK`qsIInToETKsZX+L>nHlq|wZB ziu()8lIM&4EN6(XXOj6>IkzO>!Z}?Hy!fFXRbtY_R(FQG`E6sly21n0l~-0L$Wo7*Hh9OJt(pG!S}GiAoXhkqb*vI-sJT z#m48v2<}9-e;F13bo>-<)~sAE(G?sd#wz;+$~YrkTFF_gkA_y@TQ|CTeG+tiNx{#6 zD%QwDb!2y3;$~y@Mn>49D#wb1@Y%c80ULmjK&GECm2vRlxdCUwhf_w4f}5p7e|9%= z)n~Ni$~=RxSD7dW9RceMjb%sex0&%hnI`e;N>)O)^hH72mn%!1gemyv{rHL8M8-oU z4o#M(^XEZOgsmkmBf-S7I+%-1(;N-b%%a@XE3R_1n#%XMqXg3*2) z%}pP4>uB#B|B@|z_(Pu6#|#thcqWBs&y+9)jxH*P36uk@8t_RZae_$6AU6cyj6abB zOODE6keGo3+*<>L-@*i!*uy|^%gyNNi0|S~m{V2Eq7v5f1S&lbaJg{|+lhJ0j{sTm z=Qu~dU~Qp%$27fnfB)57ycubL333)aAFW%tTHHn!#E&)hHCw5Kn9Q^l8{rA0b#Rv# z8uJ7i@eNEYZ8aM~2Ue2{AH)>&?7RQD8e9&IWKglnGkpmwhyGtr`$WMM^o+&DNwZTP zJ49h*Xar@AdpW1zr|)^U#P7V{+On|zf`7DYX~9UV$;KFDtr$<72qh&%G>cam98Su( zU38Z_t~vY@dsKnFO>8=GJo%?5)e&z+g^O$pgB{fc<*U`!gVO!DBJ`j0UiXK?I7{7R~I}v&8Mv_FYx9S|#my`_1t^L*3nN-W5gO;9v5Z6SLHv zE+7_9V}u1?vW%1b6pAe8zM~j4A%(*IY8ltlY8D%=Un;KnAsQ+aS}`W^nRRz&1XEo` zv80!`B(7b|WM#)Lc?5?PPNqTXz_Nk?rUF6(s2B81#8Chv%iuE#11-RRo~fEBha7+f z%d=ZN_k$lU4C~*3RG5=3=OoH{_`zoRnByy@T%8@NKi-qxSa_RSjb|$BwLiu=m_zMI zzeieNZJFS%Q!UvqFMTg6GcgtcQdATPqMH zqJp$bm3u*uab%$Hzb!cKRxb^^J3)ln7<87t4rLKIqJ_I&jM}Be+L(CmoeT2od26+A zT2B3FO}(-hS6!8#)dCBJG5qR!1D7VognV&4Ku;CpHD&h@|B-KexF~R1&Y!#FDxhn{QvwGc6wgw7u_JQQe-4lq8y5U zq4j?u2{j6T3{nyS&Xs}yvG`wb(z6CtJgL&^AT>y@DSYS5I$dk+tp7D*@=xzcryPX@?v{&7F*dshKfSQo zK;{!@_u@y(%W*%Uk&is}4d798o6Q)4ILBcZ1Gt0GY-BVV4~~ve#?qwSxe=mfPl6e- z1gTf-xguBbH&r=)V+1brQ6TsrV`WTqxM1J};G+K7oA5uRL&izg=AOg{2L_Pp$?nk!0Nd_NoSNPG~^hcucZ4 zX$JSiH`@W&NM`yGfrBtH2GwBS^w{1xc|XNhVqKms#~dgtFpWYKf}sS&0Z91^C0woyi2gahaB@y=RS!^BzJRNh_m1g^KFHU3TbRoyWnwv=B(6^>ys2ge{Kz}2 z;x^S|q~u0g-szm?=d^kyDT1w+g`lPyeP8Qk%L=v>>al3f;Pr4s*P-d=Jby$CF+0pSyE$<1rp_D9 zye0ojHf=)B_GjO8Z~pM&b6fbvhX%WX`lbZ8J+c}}>f1~f$r==dUGLSXb9%?BpU1JU zcN3QvmKyrQ%>47p+hl&~&}RL#dc&?0H;E<`B;@28HR~NLrOkMnTVMl8Gn2npfc0q2WkuIia9Y*WHiE zzBqhb&S0N!^OLp@ScsvVBltgTDs~KNVEJayh(Q78+Yw8 z6!S;g3UK+9_puxl#Ltc+5<--VsS>WLj6yxTyXU>LG#82vnK=yT`Ax%!09`50&h#a9 z@maJ>B0swp5+wH>5NqJtqXuUG?FUd+1Ub~2fXbx-w00OkD==|VYXp3xZ(GfsKdc-3 zIb|!{R6%R+68GWE62dmF504F{y{pbFUM|vdwPoN={mTRiHk{jPP3ehJ}`i^sHotWo9 z12nMMAPNJ>1K^zMUQ6TW`~*=m2Ui!K&Kfl(S2h7?2k8G9 zzaa2-5Nt-NySpfJa(wSQ0_f z$4v7tUQpLFMC4FQ0YFBS1(X@k;UBWIz|x`}8O1sl72z$@n&UF0yx$!05xfxWCE%>i z;n~pyMDrldQMWh4W5V%CjwOfuw=_s2v@(*-jekkDM}>TyO~`klIwVMMEF(3pO7BqE zO7q$)UFz$4MCLgqe3Vd(u+P9?iff1&Z zy8Sa+y~6&NZgRA9flau;`w3V&YpQszCk}1aM%)?2ayUwX4`ukGH}&n}qv&TGN~QN{ zrga2+*-sa1m{Eptm#2S;0@#dxLabkDz%3mk=jeMBI7J z;=w;)X7!O+Rz|+oJiAhwn!l1d7qG~U>SpVsET(uad{V!j(0a4s`@30v;f#f<_BKm_ zAK80fH&3?q3f!Z-^7#s#*K-U`1qpD1KB6(+2Vo0T9>E)Z=UMhkAMEQtGri}mBKVW7 zT_a9ZtP%_u0nM}og#IrhXHovq3kU#01`Lsgg!1=*Jyf2F83;~D8T&-UKdBYq zU&a3NCyxImWEOK%9X0u_3JAO(`>Xi2ZNL9^Y#sUY_1MSp<7AJIYk3;43(;z$Y8QWo z2Ma9O?-wula6cfW&?Qc1u-_aUlFDDey3pVXW{b&i@9$!+pJlK3id#oqEuXk*H3ZTR zjY$eU)-kj7q%>-#or>k6`_|gq_pL&INk*_bidctEfaB4_tDi`y-##IRWN%IP^Gkwd zv9vL>?~%Gq-l8M^f8{5cmxNttl`#n5s2Ehm0T)rl0cZg9oT-r70`vyh#{jU;KSzL! zqgH?-xEb-7;BcSI9M5?^1{ki!+7Q(pqc~kv!B@as(~1efm|@f7Hr6@dHB-l&N80qk zs>XKPV2bCP#R;UPQ?EuOf|0t)-pH@tg+B`{dY$>0BLs<8iIL?0xKgT8YB!oc%7iQ~ zSAGk4z`SmBUS4W|)Vai(VQNmGDSW*iTcZtw6PZqwyHJVu*-Ck{B+L z8%Wlr2SrmNi^#MsP0B*<1a17znw9@ZoGV~|1^aysC7FqG(mQAFPnWnRuF4VSAOJ7| zFRqRNA7o+;c+|6e2P7I$S^=o!6{#J~Gkz8UC&DFwBd=$O8pnMOiGUrSEd@Hd)UH=V zzVc2WTOK4&w#amt?UwuEFQ7pTXyxI*N+UloW*%^4P97S@=HsLlAk-^I2Amk)X2!U) zhrc$zz(s_xFI5v)ZOTBCz4W@(Nw@l$0|y#w!;R4(brjZedwzwC%*{ zJH*L--L|TUSh_aYpRFhY7@1uGy9Qan_g5$o?=haJm^r1$q+}!2EuW-<2ECH>yAW;1 zHy%m;htU$GH9MrC>}uB?J57}b!VVwgq*WH|owwIsCqSi+20w}BA-C2IP(0@@A`R%I z*5WYrT*xOtPW~6k;9<(IS^`oOjh{`3_|KG5|FbI9vyI`vrPyjW=?)`P@N00t?VRE` z4i?lNWGf9h`Qq-mib7M&D|eTEP6~STtA{GJOdjjXLSV})GH!^>PN3g-AyBr8J8A!W zS0m9P-U^DTPilq6Q*M=6t$d1fVV5Ztt#!-M>6PzTPAY|W3FP{j$Lu#fSG5Fw6ia2i z`CYUg?|&3utcw-T#F7>5K<*g?fldl+ej6f)^T zP>bMWS(bd9!BIRj#qQ1gbVlTNv5Z*Tp9SRn><(@-MT(aUCqU zBignlx_qlvX_9Idx80VIjS3mZahwG5MUd9sKjl;W?9qS>dJdB?aW^5`j8HI;LZTL+ zzpsG9OGcCjyj(JYSsC|VMw7EJRX^Xryvtf?#;NyNXH#XL=Jn~{Y4pvF?c8p~cB|HJ z`uO^Fy>IJHk_r=#)v_P=SN7K|6N+v;(lv>`U0bAKeEGPJMxPav&3`-MRzh)=>0!V3 zzzRmrERAHXWQ+Y}V;L`0lt{g{$gP}%gv|3=mdXl+*j~=uBPzF!ne~nWHg;N4YKy)< zq}cNZiV(JhUm9A!dF$)EmlSo@7W@v{l5>%-DY*6R;7_puqDu(z3QruJJHt?>I@lmT z7K$ElQ^Jy3%-~=WpO+JL=FC%Z*oCmxk9pM8DCPI#&CKYy3aVN{r)Zx0smGg88#nT1 z|Dg|!Nh4&D#dN*h%uugx#wB&^z1N#NLe5#>F(s4)HfpT1t^2c^tiPIktBP+O&rg`U z<|Zjnuf{>TFL-C3m8C5Rrk?LH{VYQ25j}u(0&e24)Q%9q`Ngx;mU;#=l_rox1qJH@ z!5QdHoG81xvj2Bo4oN*iZaJv_4bqz7AF=s;jg4{ax+_U-&VEj zv>`V|snFzJ1OK?c>XqT=5*t%7>hZ*e{y0ZR_E~9m}dJCDt8Fxv{}Ym|Lc= zMCP%Nt3jg?#hzD9%<1nnf)_x?)-@Y#CE{D8Jy<<&b8XEDx8y?Ze2(q4GXfs}qx=jcjaLVHYQVUXH^% zO=%h+Rj~~Q=U_FUveq-=0SVg$Knp;Y15mFZXFX3ep!)ucUx3vC5rkx|iuBxu)kJ*t z15q$)x7Yg4+=)Bh&ekfK8O$libdtV7gcRuHU)DL8$hB^fs05z82A|h0^~ZL9j^l1z zXb*S-G4b=8HcWB1yvZIrxdB(ai+o?HJ+PG(Mm`KzZxzHpX1ndNOm8KTc9TLp-QbWo ze#jD`%_7yUC*I+`1+Rlc>mo5odzYm-iihpu5B?j&pI$5_sz;FMHuEV88pDAbI zvHpQew<(-Mk3P65^3b7ne>YcgWB&wVUH4mm#BskA{@p`!8%+(Y_Eb+SHy-SR0=gOp zGL?T~>-iU;BLLQUE|s7^FsS9i-YGvBo8(f@@MbBY4CV_MScwXOBNWZU@J(Sa!4Ui z6$qEnx6i${@=8Or-=_bv*MxsRI^N9!%_cF;(%Hu{#k0zdA&iKw@0(9!WtVLm%HMX_>FPN5O1}eQa!Qp#5)+q1F1!8HRPe%j>iFzJ6ASgCeX;W$w(+SVc`%~pgUe6YJ{&;>QHDGu77D)WFDpW|8{&?HbAQr-L8l)C;t7%WdAlXGfM}%prbl zpApY>mm5l4_jYG@hgj(g&i)+ulDqdpAx(*d0vwQ~`}lYFF(h{^-h|pL(Um*FNu<4e zu@@YN<-dTcny@mEnfw#m|B6to4d|>5664@<+2_IqppW3d1*xAjQzhxFRRNg^_>0!O z`5oLJb)AX|qj@PoJ>eRLYNR*b>>O0~-}0|Wo5q?Sm^(iGwLhkXip(iO-)>$P;9$V_ zQ}}&1xNTou;}L=>lno>jR~h=IV_glHXr1}hoVNrAXJwIe(=HV-&ag=1pTD7yJ!@pCYG$0U`&|CxSd3+xR3fiT6s4PGKJdT73XT6K!IGLr)0JpDeepRT9;>7 z4SN($;1Ub0`f5@eV?f>4rXp&lBGm$J1SH*WuckS0pKs?x0GjKcm;zJ)ayax9E#FWi zxep^hqFxoO*^Rw`iQ0dt>-A}c?_>L3%V!%4&3J|>!Im;M!};L~FLX9x0SiLN6DTeC z`i6km zkM1jQc-VGuQFE?n?F$>ND6RZrXdq9+orG#ARSU83L+GTS%;|>_lu0&ose*M{9qwW> zSkOc+@X~3Mbr(o645IN4E&c3|lr9PIHJUH5Kc=MJk?8Y&wqU*#+4?C362mIcK?mGZ%jF-=rWJATN*%gO}ziPhA(9NQHQ2l~$}hW*5?G+)Sr4U}cWAu`f8m7C6f zf~6f<&|Wqb_g^tTfkfwY!wvlaJK&~{xbd}PltCh-k zz-;&gI<jNqYf)l3H&loNBBz;)^Dzc_3 zTelmlRKjCmd$6yybpf+4G5Q?o2^D6OMCcgg7oC~t7EoXTw=ZY?eQ#PLpZJilbXcW- z1~4!<7pTZQM?__6z`yI^JCTEa!c;pnPa_)NM(Dw9;(`l3*QLxlZXbjM<#RyXxGas55Nczw$Ed&+|7CIg9Ip}2 znKXj)OpQ%*Xkl7TI`?h<>Bfnc*jhLyFx8ilfr|o(hL)v9CSMo6?DH}$wW%)MT?+?l zR}(gg`S$@-2KPVHkK>rU?K0=p+7w(3j+=Yk9#my7mS6VPr1$=k&trjS2@W|v-=W>f znpIG&;#!K^YnGnc7cowoO+AHK0}mPazHG=uIop(4XP^(jzvidX6xH3nV!Ix6u;tC0 z#nm=aBuxJic~N3uuKd-yuVXEb^nCP6KuyD@VcR!hzo?N$IL%*;A`g*cZxOz`^!+gg zgF$U|bPxQy7UCnPt{+oMo;3!)X*9Q=KxK53$D2v=;|2a4i^Qg0p#%~noCm6ZWAMB# z%2PA-T7UTsio~2NP3NgMM4wbuY5Ff&&94`wR<88=R3#M7eN&-G zD^Vhh&Ozf|_+}Xk?%N_I=%zjzJo+fpI9VY%+1LbCb02`#LHA9{Y!dW4^STQSYZB z*f)e^t4+vg%sbgpquLQQ*-55RP0eWBS-kQlvvBFRT>W2RODRqN%!vLRY7 z(m)NH%Psj6=<~+;&#L4AD`I+81zL|+yr(c>!5T+&GtA(kc@T`+>Z}iqG}s=*(Lx(f zFS#{;dFgH-r*Avi>FenzEa=Cvm+-TTmy@G0R=U@n>En7wLJX#l;}~9rk;}^E{Mhte z!g&1yuTDC+%AF%pSoX|331h9B*36@iq1zSDH|!Y6DcqaF1+ z<2{&bv3e1b@MD!f3B^MLsw|xMccE)h!=IS1`M^$Ydh$&gVR0j5d-PE=M5xYQeMOKd zD3Y^by$$V*PxE5)#^DlZHE2$PO6FO;xYPVG>3>Nza0cPuL5QLVjKODR7pXQhTToFYL}Q45yW zdiKzk+L)5U;euds{WSDZT})FRR;(FPADZtwe=P4uq*mBZhGp!Tx;TK8bBH?m4fnNQ zh7orP$d7eH@+<`xKT5SW+*cM@6&gw*A`{_lX^K3O*V_hrB{Yd#)t&}P3<{{E(yu96 z=uJKR;;h2MdAZ+imSfJ;!`?~W>yMY1*f4P0|LT?Mb|`^;aFjM7(>$hnHFbS8i)trc zgFP~%aSP3!wvH5}AZQT?eE;53HpY?o)l`{ZF@wLWIw6?}C-lD(aFO;p9i~mD8A3SSLgOq*S!7RfN z;oELU$WYENKMVahFDh(Q2U&fSkKVV;7=J7SlKG!N=o5)M+OsC9D_tXr$_i-jWJj^b z;X`_w1_p(VL!e9)Hr~p`6$?YLzT$uFl*9Smqx(`<6s1WUzrXoAcpKC5K0ryC$h$vW zD^6nUL+P@|6_IW1Bx?nA<8}%1|FVow1O?1*Ydul1l7*?-r zuQ}EKI7T+wIOdLFvRuE4_jSdOWoSt1iAG00`E2^>Rtrf3RR9)~wSHN6H9u%$H~n?W zEs|!Dr8%f;Le9CKixFkCydVfuu5+;{YyeiDl(~ID$M$QO2NGAXvgQ6YtIHC2N-bCW zM~XT|1JRmuJ+h(vYN9Qx0Ye2_VaiBlvhEo(hHuLNO~Xw}#J(M78%^PiCJR)9Y00~~ z=u2)SJzZHzbY&R$s>GydrDw4+p;8y8d*y6A7avQjSR7^Ey_&3y5=34kxvU>qHE&SXUqt{S$;6pGziJA>EcoPpQeq@`Yo32i# zYt$NV37Lm6K*NW2muo~xF~aoO{KA3^a**ih5kq8t$r-mDXsO49EN#A?s_boHG$jG& zB(h_i$aV}kp<(o6>rubCk1~k1drOdOk-D%Z8#kbn>2dQJBTVaH^1qhTXJ3dBQ`-ECdA41$* zy1VTEE-Z&0W<7zpj95QM3)|)Y!Xj1_5 zJ!HWDk=?`aa=XbVR?9E1-@-HBPvfRSVA z8X?-G=I{Mo2o@~do|XmkD9XO*wm!`#Ch9a+ATOfvVE5fvn&@H;lu}C!T(F=&MNn1G z=KW&SS&xQyi#tc$F^nnXZ zWqUXP3cEPR)jfW&Xg(N8d7N0de($p~y1m^utX>Ccnl_8ooZZ?`Ssy41VN)*jgN~}l zaq^Q*_cA%Wb!?Lws91tnbbu)nD?d~(}W z&6lQpuSMVjOXY>g6R2mjsF(A@uY|+SlLcSbKZkpNRBSSTJFKj1j3Md&*4UfBVEC$t zs776zU)F?tB+`FqSc$p9;#j(Kz3)vXjf3nyan$yJ0%_FA0b~GVa#)gC0iU(LMmoDH z&@SKR%<%VsS)*jBHrq$J4v;1>XSuVhrE+NIWE5#Y-BfCQfE5H%*!8(UcB!^5WJa zZe4+kq$D9hGUc*=?(qpEKZQb<;N^H_W>v>p;ZwMkuVdVvl?c;8a70Iz6TZ9Jn5LM? z7|y_+{&xQ^#&FsrLrV*$WcJc9uVQ<(mX`4g+@=`p`;KAZ+g_^N+xF7Cw~=FPUcTySbAc@Ohe?BuoB{@z39gS1-IAh zZk#8xkB>&%UVc8cD5+w)xub9`omVXRV<}bN@-3g=mFDZZ$MLt;n~MvCjPv!4;Y67t zR{dLujAmW-$d5OHc*L(4XgT^Yupj-dP{!GWa2hNaiu;Z|Oo)e6oh#Ce)I1C8Nxy*% zZ9Nb1Rs;`&D`|zm<$aiS9OU0_h>k!aw*`)XUOq|; z>4#mvG-d~rKtF3eb~pWAubXM5i>CFk-;Y1X35oYwO>>SU0;a)cP}x=Y@M&s=HLGrB z60A|@>|0`yaAvmxMw<5~M6Xhpv8wjzbn4f@_C&RGu!1K2_$Lra$ik>l@M38wYg|P>Xi` zvdo9@BW<&E`XvGingIMhimRU#lovMVmwt5}%yi9XBjVp!hdKp&-;3zTquL z!A>IZcV1}vq+9rW5xPad8uxXu-*vuOOY5@+NXY?m&4;j-Jy<}^h^rp{B6vVZ?#%Tp z%X~A={_CRF^|7GyrmFk(kwE{RT?L9$qK%`bcRDxpt79C)0BDeX>=zseCzmJLg|Th< zmsM=1Xlo24vbfg)YZ-D2{#&~)#ZMph<%KV0;JbLAk>Vmv8)$?3pPJ%ygvUg7x?JR-h z4!KJSG}#$qv-;^J_emeV`Oz0l^f)Ud&hJfQqX$mo#oIg67Ve80 z5*t0Q1AAaF`l|{%l5Kk-!VqTlQw#P!{H;b1e4F>VnehF_u|7%GmF6$3p6vIvt-J|+ zI=OGd1?cHfn7H=#`W-9mCbNFz-=z>-RgpQm5WXbMdo;KCA}89kgOCK2a3F(xdLfJS zOxemB;UJo%i^2PisPSu$_XfLy2aCM9_!EhyUrn_mK>SCd%VVP7&%fhjc+qa*1eVYd z(0w?bDz8rhB|N?+y<{7K`H#vt8sx4mQi^7R9TdXXqBS zLJ%1vy4={e(;Fm&WN!hZ|?DYs)~Lqop}n-_B) zMX?o-_Jic{5Wl+=sxusuPg32dt8b7!tp7SbR%|4k?iPWq;Ua4Qli$RIYubtGX8Y*k z_v_Sb3t#=RT}5h|FP@-c+R4UuW<5$ql)URN^b0~3h_eJ%bEK$GWMp)Ez%iEYc7K)a z=f~yb%Gi|1uHT#jqall!hJ%qMif|YR@an3J#lfaWyMHv3Z0NjaWsK?~WTu-6y0yPe zJlK}41Pc-YRVF8U zleswP4`XW7kq|PP0*YN3UW=dBD)h1-F^;dK8Rf%_X{w?iL%Ac4kUBVQ)WwT(E$PwX zp|e7*uVCPJJa_JxYcEeL#renSZ#+c#vAOU9?}FH|`LAeD!pV5~FA*~2(_zT@6@(dt z$r&RpFqz_hD{l~%eR+rX^oeH-0qq8&2R(O;dL-izie8?r);E(WEnXzZrA9P&2r(*} z`>q6e_<;)wTo4T?%UPZo|M)>?nOL}{viRFoEanJugJsC_W_(v`eR4>tM0bcpKgA~} z{>pTo0<|Yl5v|(vVPL1OwUf?b`6&#xw8D^B3*Yzgx$@K}wWWIy*+K#{PM<2Jug)jCCC@$9L=FlXluCNhc4=tsSI ztLk3Yq6`(eq1a(E?}>b#xs#PO2&%Ns`xrjV?|=G_^^Y->tf1N3-afOB7;oxoe59oF5*RRF#irwUp+q@s8DS1zzs9d>(!c<4?dG6Ma)K)X%zPNuBc>1usQN5 z$}kQCi8`=z`Um_z&6vz>8-VE0^|rwD`(2FOs@9P$g*0joke&M-ymwZb*Lqb;-g-9+ z1m3f*Fq0$XZq;S%^WF@*29_@CiLMVh;Soh%AlTxVx-QLb{*|Mrbe^mIJa!`-zG%%B zbDh+Q$=j!l$kCsG=j(6ugom+PgTZE}#}=`W6f03} z;I`t|AenLV>Ar?xt#>SwCmRMzIrXp?ParHS3-n&}gAj%deHOFYAQY2chrtB1*t#SN zzC?AMkz6H$@kaQy!sNzJ!U*-NR;gnY4Z{?C&GY+d$Z2oXuV`OLkiC#beJE>?pUDgB z`^KSBBcXtAUKdt*iQ&s%Ymt&hh43YRHG8Iv%?O)s8us~B!~+IK1)i+cjhjMY^Ou^~ z52C7hZF=JylhE8g*g0!lx|6TB;SS22|63e&8!VF2SZRiNy&7=K`Pira`7fwfRFpmk zS?)ARWEbu4y6c0?WGm_8R;>TJ+MkTF4^p3p;M}?PH{EGb+Xz}8CxUgYB#N7L|D3tA zg|Cn>>MSxe8N!Tg<75#-pFjgq{nRl>JimrtCNnO~$WSUZOiViz)P0to(QvSwTKaan zw0Q*13(5%}o128LeV5r9yX6}FkugX)zh!?3bI&$qvuKgCveKc`Q2v|Ek!9@bqba~S z`gWK+mV%~YD4=R~Bk4obN+0Us`_T{k`8zZMlnx&y%1`Chz=|%|nh+TZI%a#17UJd8 zMq<^hMAzdi%{w}50@Z(T%&c3Y0Yi*7utRm^Bl`>d!A)AKLO- zcVRPIoTs|@A{xVnWEn$SBt0TMha~>K~2r(3_IUcVy!qcTnQAH&YQjgvhQ&9EZu7pt;gp`L21X ziN+^~9r`962WV7BqBE5rd1{x!2Ry^MxTg*qNs%p%;K<{+2KhV6d6V)RQDcrO9OzD| zwf{=2hopD*p(0`i1J7i+P1lE7%(*u+g|58}ZW$iMX*WgYuTZ!&QZ{Kap{s>T)*cZ{ zqx^0^{4K2MtiH?HSkgfGxfU{YIKOUnO0O}<$>x+`{6aFRHBM+#D-eZnqa;&(pG1DR z!iWA_E+Ml8A{n>JEB&B_lcDTxs_VhZZ`jauY59Y;-N&Nl`YE=YDMyl({cSTLDvWKv zJ3O=jWt7H1pDw50z4>yX0oMwy(BV);%H`U#wCJIieH%EOL+$T}i3*-g+!_ z>P5Lr_n*}Ddpo_T4Yi?luM0|AZ_GU3)=&EW{N(}L?SVrv?>TZPDY*b@)BZPI19`ia z2$P>)Idb&=#o_Eud7kYRcG=?^^9!V|HrLE|-I|}}6Ddy;c_2Fa7L5oSwdM>C!a0E> z9Jgs=&9sY3t{o1S1a12YgFK9{8{<_YExsL6T8Fz4%W=x}=~108O4{WXrc{O(D=MTH zH$ODYeEZ4C((h7@TmKn7-?$Q)U)X2%GiC`SyRVVg^_3?pc~2xxDkDCl1|rz-W+!7e z229UpDLfJ@N~b4Y%iv{&nK?&qhugtVq1GRGQ~u@}mVq42;4jvr6N&NK2xnZ=uRvXm zOfyN5q@F=LwG&Q`ChiGzt|LJAi`0Xo+%-AlEpCE~-_4@ylJ7$qnvfp+BaI_{qz@#~ zRNErL3*l^wtsKrmq$Vh!u>Q?lN^S2sJ-5@!B7}O;oS9K59PI)DA>NSxaVD&(#gd-f z{o4V%%-55Tb`aqxlLHVD?M8Jej%*03hT)>!a31G}{mkeW@r_@Guu*8p$D9p66foDP z5KUB+%QgzW5BrJKqB2w-oRTMlz@i5o7$a}@S%|dBUTUi#rZun4X8cgXdSOI(<~31g zDy7;jP~5yV-xnMGDrBRtsWByl(+f-`2_te2a*ac~DxqCZ)T$X>spdQ+qpnS%@+2cR3#K5%KmL~h57j_ltY%*N%<$S?x>as%z7LxOWpt~o~ zPK75VV}6uof(Ck1yj_hu%tRpcv%Ds0zDDe=Xlof}VwA=kGw7dI*69T6FP(J1Q14*X zN&QhCvKvb8^(nb_=-ZH;M_hp7fn1hF{v~#N%T(pX{fNoBXF^oZ)$HiW5S>u4C-~8V z#&(Ao|mcr%ECY0-0w; zLTF;q^$~mWv_ZDo%+-0Lx~=p}bjkC&x6%BTUnnnKs+JPr_$j6vh+w!$(p!kn_Q; z1NI9B1+7_~DSXv5RIX!1#g8eUe!!$Sn2KxA3$oZaLW7`39Djiu*VPzyzr#LJ)BBYO zMTp$od*`hMgM+e>W;>Fj@1~v*d_vX4l^5yUUvW z(r@DKMGP4@9k{iyI+@-eR!ii+ydtP^It_Jc=5Pk9vbjZ6Ie_tI{$hBKZg4=sVZ6+| z<0$)QFGOSr##o$P!Ne?%nsLSTt&?xNkTB>*pf%xZ-Cg%9a-hGt!RJ9PLK-KGx84g$ zz4NoJ;l7M3i+X>&W7JrG2E0g^RMrxj{(=VELL@WK<}WAYZ`zl}3;&i5MoubzXUxXN z{W~H=>Te?#5}Whc6Q~`O_!IZfVr_ri)@aPEU|eAZ{-Q{7dF`z>i;5qdv@(tvMC=!= zL5}$0U1-*<%f@Tn4A{I)Jm#Ow-_!d%*p;&tES2S>33kOWseoBkm<~f@qy(xHMoGup z*YS={TJ6+LJJEzXwu(od>)En0ima`l#6EM{q3EP=$9=7}Jc&8g71{4oRm}PNwZ@D( z=JhFvk{6XHu`lDyb7`ajhuJ{%O}x7MDAc0ZmiKt-wAjt)(VzrOVL8khuLdEv8C-k|mH4 zFf*C$i@4~e=hsg{I$asE4bq~yizUH}2yh8m*~o#mnIf4a*Kh5D4HeO<$&7BX4+?DmfMVXSt#Q&n!TC(x(3UJLBlQ#=QqmmVa1 z5+U6JtLKCLK4TxJ2NuD6zY3aDhcI|E9uG#a8crG$7dVKbH2XFZTU2~QT>DV_kjz+Y zjq>{h%?LBUm2&W4dqOCAq_51c3bRAx|`EL9E_H)zJBmJD5qb=`u& zKH-W;Lq}}_o}B3y0-*?WnWmj99;lHS$$?d<#MeWvff|&h$o2Td5L7)zC)#jr7io}F zr+QXAxHm}P0tm83lH4{qA@5BmOtKCoh;OyCm##PNH4QDMBT2vLND>n)g2dVl?rRD^w43J>30?EolAU&r)!Qs}<^3_A3_90$&2S;9mubEoTiMv-Ef+9Om-) z>h-V;!JOW-vcrt*pR?Z)B~-h%5DN??9Nud-2Fu&x?%Osiwtaq?H~;&Oh%R@TMewoX z8{Yk-kHukg{G?-P!Y^k~`7HhUz1-10Hh2}TMPLegc ziMLPV7_c}SM(hl+#~}7Kn@KKVCf}ioe+CnSBAmI86#qoIMX{$`d!ctXi ztdFB^(m*aIfTL%2=$szjC{LYdK7PNbrY_2XMdyIJOws`OG~z~CBxX5gJ;qztiasUXsui_aqX$pvuvyD&9!IyAg0eY0XFOvS z7ZWJ*XgJ!Y-7ixr=@tmELa8f53m1$T-1uxxPTqXIhiZS3DyuQ@)th#|52c4qJ(|tk zjJs{wl(T?Nc;_`EpC=^D^e%@z(U+Gtnkq?ib#(-@TuCHndq!N9P)fKMR<7KRcjY33 zXt2C>c9G?{xRAe{osc;!Jz8NDuVl(3f>5Q9sbUEXNA2laGtO{~cQR3K z%M0$4((W!{klLhA;xU#*To%f<0s3bd`_`CoGFNZ@g%n~vDWj}@8rz8BafU!i2m0|- z{Hfj96cQ_-{vYU8_Zy%WKo#0U${kPbj^m70f9B7v2O9+FdnhLH;kcO`ka-G7$JZG> zs!xk^*`!Smxq$Rs5Hzi5!a)9S3PJgMy$A!gIsSFBJac^4#EuWSNz`2S2c)X3 zV8#q`85xF29AnGh6}~)78+O8ssHC>(t!~MfO{ZP0s{^}YDPfQ~6U*utb>|NI~ z?hE@^w5PDXYukBYkjrZ{cD{I=klDy=f(ncgSxRbdE= z#4C8>$gcEsWt5YWPD^(6BQ>sgr6$^Z{Y|mS1nj7~*6ge-uA;NLx0sZW+u{dZo0o3f zHb!uM)t7|ZTl`F8B$mwcl{-q6;gCJd1W7@oiXFmIIwRQj&AVw6N9?Zq5XoEXI z;Z;xjMrmcSm&%@G+wHi4{VAx1HZg!ar|M54mI0Xtcq8w{0g2i$dC%=j1WT_35C_!a zmI$jZP7ZQENT4Ky6Xo5z_A~_49o}C50I9$4KnWx5Az6SwuP6EnfL{>A?F}NYzT-e9 zxRkg=mQjvNZzKEA0GdN126;Pwn3@9^c4+*pcdrAQ5N1ylWs3q2BXv@k!3y|nXWUQ_ zJPhntQR)sT2;~^z2R@+iX}Jiqsthn>BaF8L`crT}6O5F~`>!H^$(0StHgV{Ap+t7} zJbQ1ZrYyxgQB4~x&_wIol<)hB$xWeUJ>N~~7jgn^jC+4fFSSqi{uWBuy?0F{yT8c~jJm(t4ZgHN(gs|I6_ep-u($<|DoQC&YqK`wU59Dhjd z(0|FTtBPy>v7q#J+(-l*e>&l#Ff0M|A9?_00Q1}5J*Wt@%f=5l;{!AV9&a!i9lIWX zpr9gM*~S6KZ2Z5{fQZNg2XXD?=9CAAQZuzbttbvKu14Y*0m(H$77PvwxIM5iCK{{T$@I|$`0g-%cJ zKqq3S#8J<$txyHm!F4%2gc$z- z!BC(uM&QV}$1HMa0F*1-$+-GEiF^}t13oDP+$3C1;4Ea=+Dt?%#1rhUu zw1pgIfQTD`Rsf%(G;5&<(ggV#|TsGy~2@l!gFc>H Q0QXDW1U%sLjL=#C*&nqXYybcN literal 0 HcmV?d00001 diff --git a/tests/system/Vision/fixtures/obama.jpg b/tests/system/Vision/fixtures/obama.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e5128d3fe85f259dd3a881dd1837c570a9bacaf9 GIT binary patch literal 18809 zcmbrFQ;=mr)2?@Ow{3IUwvB1qwlQtnwmogzw(ag|8`CzQ6)WDV z$gIqI@_X%j7l0%s{!<(P0s;Vl{2PGpO+YXJ?0*dWpF#Y`fIvt{AP^P``rm*>hJ}Ou z_aGypAR_#GanVsx(QyfJuyF`~kdXXfq+{ggR{{C|{lNDC02vB|4R8SlLIMCq1_46` z`5pog005w%U?Bet@Lz*~1P6kGK|zB6V7@m1Q2#q|{ftj6rT{C`!p1gF*aI!xps)@T z|A`*9uRvfXQoinEpso{k=j2Q~lLAs+Ort4{tY}$iKbL{9uXR^p_8{QFTiO^2&G|O$ zry!i|yVirr&fWh?p(PJNqz3n?Z`Ry+_KI!nBcN7VP}7iU%ID+)JEWAk4|d7La1UCruior(1 zIJRW)#1svqr^sj#QwiOj?|h1+s}+Wzt<;T)flEi(%>PiONxV~6;@Bz8Sg+OW^6_h) z5I!0{AgOFH=eMfytO;4@r0fwt-h+}4#tzFR?`>2qHZ3i+3XZ(mTKnrmY~tpe>ntNE z(2Pk3Ug9t!tO3K1?JS$53M6HaCrLR;+f(T*zY=}(A*#{vbI#}pRbgk&*s3;*@?o|S zQ+eafzZe2P@Q%c}y}QnE^cBy3OVmPDKvfgSWo!7e@S{3-jKQ;ajB>V3A^p}QW*bYd ze%|J);|N{C4aaXTc`Kkj_h-ff%U~t|rov*(8nkj8INX_(%{}Kuq&wsP&v$ z*}TXHHV<-9fm5@kUHLZ!&0KNoPfL$i1rDe>~!{dIq{OcrV7fYb8N3aZaf`_4S+ z4c?x*9=G3GD4tSLY5!Ay=7lVp8LiG%M(X1sw9%Tcm7zMExzYDCB3THO7_QSS|CW{;K|x8gp8yk8q`> zV6iPsQa3wOO{zMnxdTRMQaUpsEb5TT|8p@qq z8^QoF7g%Ec}wOFiI*@sgY;T8&`TEtL=eejo4oJLR*1+z`RF zEbx5t$CP4i(UpV_-oQrJ#-tvnGp*D|+kKwuuJE!Bl0`kgcpE{c>SL7`tYG|WFyi`?epdF5fuN~yI5Q7?;f5?e zDCb}GEG|uCaSSj>4g9XHsQoByRLYpT;b8;sx(A|K_?qsjChIRxoCvXtvzmOa#_=8t zlMS2x+R6kC!ravjOK_>b+cdt+Ku-18w0E<=*+oat%G^(3)8mUOBQeGbA`+s{Jcr>kB&T2R+zWv!dzN#}iZ8ZB7q-I90Vy76N#-YMuo=wcVVz zg@K=~Hdi=<@(c{F02#@s*vgX)d7@NX1gkK_`ZtErYOimA^(Qu6N}x(rlixoVhYJGu z9}o9`9UK4v1_25V2?7BAkADM2`Zp1xpb-_}m4Y2FAQ0p$Lp$R!1)0pGFvol;0X6f> zaCaZ*`&pb(FqMYO4PTvdEi!cEd^umltA$_jY z&J*}23*Nuj1o8j){eQB6f$$yqYK!7bTQu#QH;fPuBY^SXuD%4q49MmLPGH? zR8CV(G#6Jwq9p2kqsbV8Dz1jS(-Ia{Mt)eWUyI=J4v-TR8&Rt4m3>D#Tu!iZRk`CX zK<`4Nx_=$4-HiFZTc=TArw*EP(G_H9av zh)W1=b)uC0ghZ0-bwJZKU(beH-pdlf?j<7vBH2+}T<%2WKI3}P5tKbDr#VcnXz5%6nGjPdAW$wFOTv@)faE#%Y( z&-YD!TnSVTxl_{o7kzf=1R)H6AbQc>I^QUK_v}BdXpsIzNl*|l@c)gK|AhIsqrd?W zkjN;gXv9p+=p;ZSQWjwhR-ylf%zw%*D+g8-S95L(a%~3L+^{qcuHKG<6+{KQ^kX43ETAx*XwBMSyLPc}xWgzz=YgP+i}E5ocFb#+>CP@p z(JjpUt!L^h<{M!2mAPredS#pdG%Gi?1XYXLEwDq%|e7koj7P(x4STvN-q zv&?l{{<=LL=j;xJrf+1+6W%bXul(xl&D@N=oY|<vVP&l;nFUj&V71zSv4=v;_4MA zm&mEO$RPj1(fj%2Tm04c_QdhqmX@04vZ`-xeT`ke=dku0AYpKDhq>XKpIqr?lf=iu z8+O(u=aO03a1-y{9UtInnpjg=o_S zfssKTK39uQ*xkOYWzfJ>vVFvFn+?!o)Or2r832U&{h-+%V|^w0P2s&}I(?5-X->;S zi296J&GQHF(PU^FCcw^=)E(P7{TT{kE9)Ep@r&A`YyE|-PF^lm^fOFv{F;P-3nhyRTmsK(s2%@-&RW0R7R}w2 z)wl)nhf^cDyCTO>xk=#CU)r9 z^phKP7g%E=U9>)7PenxaxHW>#Vai5S^k3V!xOc>AsOyO_iMV8V(-T2a~mqX{uA3X^B6X^fl`VBb9rL-P$vF0kdP&Z&zn#(BcH?j0#~mSdmW-#8=}9M zAQ7Z97Ukv2_DZ|?RmRTbyml4;xhn=pULkqkM0osb1L1hDdC__4#LhLS>)C$xBeN(PJAgGXtVFTJ|MzZK89>!=}hn&xR^waN=pL(%Ko zD{_QnrO5ipb1c;@6;5Bc#GAzIo~terrhJ9(PgzLRX%ru`v+sm4bj8C2@^#fGveMeZ zE<78@i$Rrbm6x?;S8g=RC0|e5>-LAVj6n|Ms_%|wEsu%R-G6erU+XrQoSGih9h%ig zz%=Vlv)HN9aE}P8q=-s=nJXBT64YV`5subx1THV*n!GJrz$LdXBFa@XYn5%wvX*Vt zeH-@q0{)(IxNtCN=<=yG(*Cl7%KP-R7j|{pE?if0*(e9qZI&=Q4J;%0+l1)RQi1k* z)I#E>^%)%6#M6{&bi^--{0+z}R;}eLsw%TrmrYp<#}FXB6hI?{n(QP(S0{F!wzPV6 zA-3yQUY2!dV9;O(++L*||JfVEvD|AaCm2Wn%c5Oss;-!U)S^V*s*~LkqnN5z+fb_= zuB7;9zsy4wXpK9(Wo{h*_(8n{iQ9i#9Xl1Q&BFXa*yK&$2E)#tad?2Q(Ym86*7=La zYgu9>L*G}{G#1^F)6;H>pr9sm%d_K#vf2hKeUgccL59GWPj1BWJo^huJ*Xmrcy%ZS z<`LqZJ(AMePQ%wOP_DY4eT~-_xWP|r^BWBv+cXpOVH5wVXVqf$Xzhd3hyo{@D1AJ) z4Mo8RPZQ*dU~SgX!s}TwbMhJ7|B&7!84TNiT+BgiqDCoI&xE?$HGpauMSl2WbP$Ot z1*17(w6wmp{8*jFi2wqDlF-b&6hVb2Gxt zj_GTYt~9{eyYsUUGMi>vm&IrT&PNiC#iGq(N5Skok=>(sb4@IAb;O9as!nrTZ*4Yk zr0tXbpgCywHeyBq^PUjrnm>w3D^Vru!>)LJ^)#uJFQ-|#TwCMQ)F^M>{%W%I7_+78 z;`(5tt}G_Dy@iD8GMTiQN!qoVle?8x%+^qZ$W#1Y&qS-cHyzW8r?RQDb!Ag++9$AT z#jc_0=+je3E;jf>eaf50_nfFk=Pv2U9xB&tCTLvwC|C9(yy%fDBW7_YQ&uWjI=BC9 z6|df(dtZio!XU&QDdZuEIogS*VcEUbwa&}B*J#IvjP^@6<>3BFO5WrI)_nV=HhnTs z7Z5Jh?WV`0W3iO1PGUI6I&2(9x%_TZ`N9t7sB*l@vZP|fMVj4WXP4`1CW75f(3{%R zPvf<(HkjJ0<>JBS6ACpw9l$V(wvWN1tyfUZdw~TaeAZXVSNOBnesF`SxaB~F2C-;Zfqe;
^Vc6s>KZaBd0} zcFH65bgn=-79M?dLAuzEywctN4IpI_u<;Y>t4G53`G|*i@n^J@SDo#}H&4?h+%t9c zi^p8&+iufcj8%7ar>Ko?@Eqaua2}EtDfJ`T@cUs6aCZ-8!PO1IU*HT+G|EQSJA7tJ z&&zgplZXk3SK>5Z!34%2w1X_NHbb^z)e&}a4E;1=)u_+@;X0UyeTXu1ON$ll1xiwJ zbzObR`q-50m%;$g4`1vpu2t6_@&nINNsJx;y~Sre;@x~|fT!+k2#jV3Z`SgsxxO!3 zt=wqnB^7(8Eu_kt)PDC3v6IRcGeP%fBfOr@IzS9(we@P=qWlbrWl@kQS2uRb8#A&`1emA&By&Eqw-m7kiY-j8dklsyt0CKxE0unv&erGld@qKFsyf*9IpkcV-(2r4KVYj zIl*SZ7OqOX5?GK;%Q`8Qu@oFxn02@rzuw7NqPUj9i)zY=s}cj=)aJI0Kb6)8=nmr2F^GtF ze@2q6on?2-)b~qwMi`3QvPHGvg1Rt2z_)St5V#(P;TBbgs~`v-zMJ2fGsPg(c_!Ba ze>k-@oXvV>q|oA=ERjsXGiE_yx-MrYfE$I%^o z>n04IomVMEXQzL)L!Q?}X^(NFdO*%OFvF77NwZ@mJ}Yw@T^eO!BT`0bqxrX7=#nYt z2$aa{)Z;rT)k5HP+O_o5Qho{1m4$X(k2vFcZRuMMzshx{S~EKwLkc9nnq0SZGr0>a z?H+vt9;wX%rGPrQUocKxn@n>~CwxV4Z->Vnz13Skxczi4axVV<@_N&#m+O8{9%;tS zQabo3{0n}V%#QZdWG;~C!RvU1Et6p4CFyk-_eijA#jThsp&*a{1G!29^uaJ+K+fVmpEh6%S7CP_pt9P&n_z^6Cw_|PWHm*VdlT=tT{WAkW zQ&d&>G~^=NK4O9D*egoKJwRsa&`)~bupG9g_* zM=qKOvvP!c$~dK@CaDQpp90zdQJK=5zml7j-CYd_x=L#%PauKR&mCYJ42M`vdBmXj zEp@tukZs!(1xS;kb+7-$SZ#A{C`cT1Vi{izI)tSK5;QngkAgoBjr0|XiaKN(DRxOu zj9MF~7Q%7P$~89^b@kz)8#pdcjhZ!^o2F4VgSSYY4Tpy8+hKdDf=0vp>ITpA)vwnuXIl_HDe?n7k-0 zU_MyJx$aIT$^Cy^Avhu=q+6nGmS8-}Y*6FWIQ`iRA`v~AbC&z9xxVcD5=$%W5~BJ7Bud)eWtiZXC&2~F3nW|!AY&b=`WdEzmFXG?& zK*KnM?7;{JMs3l8r^wjUvAdHmn~k6f(U(oRW8H{_r530R?T)g&P_-)m6$UnvjE1+; zF}%{MG6C@SDbUVUg%sKkJ4Z9oT>=+a7H?=8jhR4+^z|Y`fsQIwU@{sK0xB!piy7=a zEibeX!D&r<*3R@tl${vJKhsAC@F;l>B2h@%pC#3G@I5F zz`LlN&IPX2IpQM}GxlO&XtX9(?WYf)XEkIf+XoC_nyL-)ikee3Q}GqAfkWR+Hi5W4oKIT`j$w{X zeXKCzhGRKgbmd0oA1ZEfc;xGBQd*TOPBE?!Mg}SLVg16sAPg+$bnMHT?p4YPiAOLL z1laKnc4;7YV$`Y8k_H37&0u^m8d2f;z;3DIT_l=2RR+z2$5v~eJ*aoQi`s~A$br2o zZxVtPY_CZsl&zx?zsEi?W=1)}U}F{i0=;y$G)zpZHpo4$d)+ERY`-i&g`iekbm%Q# zl2@--VR4=~!2Qw;bGw$QgZ*~_B)`;?PwgVB-cQ_RsYzrg+V|<(Y*d-lg8)q#qsur~ z{Udhlt#-=E@O3e@4(rzOXL;!=dT413T?V${~74 zum{`h$N?$i2#UC0X^gZFL{PY{&l9sfaZpmjmh+&_kB1G0KDI4fFYjt@TcgN|g7cMz zavvDsz?UmRzo7iU@`_h#nm)& z(_s@lY@qlRoXH=l-yqB4lkO(sKolWpNmq$%qSIX8r z-UR2LSrsknhOO-48nv>W$TAU8zf^5P+gjACp6Pdx@~}c z5}`R6iy{okQSyJsKV%69m6L5zYv(7MK0JO3RFH}s_~K~R3Qp+_M~L+Vs}v^0)2zdy zL>DEnY?~D+bn0bxUOL#&9EmoaiKdp1=_JR@*w}{5)udGEa^-Q+dkMI0&DjTyo>fS7 zD?5@e!a!phJT?$ek8*r;iRdB9EEU#qd#;EF-ENqs*C)eOIl~G1Xs2srE|dD>6ZFQp z{AU(|x&1qq_;RDhv0J*h3#1ce5Bn_r1d(x6>wCr=t;XRykOjbLnAN;mWw$>Q`lF{d zKXoa!%z@wCM73fTSe+t~ieD65(UZ`C9bn+;wRya0?ADYRd7O)LFOQtIWb8TDcrg>v zby_Tj)k@X6(g6tL$x%zTF}r4A=yc=IqZ@U79A#~DWjVHS_431(hvtq2ek|Xor4EQl z-qhuVOi-RBCZn74W6*qqcbS$=XAW<{U|@LKzZ9f^XTajPo@+&4V(%$W@f*M?V!;6|?*ni*a>pcZOmq>|xp!mn&U+WxG zt{uByGl1s5P(8&~IW2-Qd>FI(1k4w_d(ny>q?yTzw%6efmBk_cU_?koDw$22u3_9p z9zRrzC4sDC^xgwk)T+eafPEuT*X|;O;%tf=Ie*6Pc)PV1z)x*$++UYq%YBq%%)~uE zNBH7>Et5Abf6w-#ZtZiBY@?peP|7mkY4Ztv0|vhV750qiwz1Wmre+wDG9|j4THvpm zLQaKm<$eEdQ$7VlTRb%#R0sIfH?Gk2y&DM0!<)dj2$0)STp|Gu1QmobVAJg%sXtcB z&r9+Vag4~_Me0sx^nT_P|3L9Z7YsTyw6$m6A2AAafr6>GNnvl&eFWpP%ky_&#uC`H zJ2lbeE|^ND;&fOpIsTA9`asQGv_L)#G2+x9-B-j{phQhfFm>V(ZB3kPQ-N1R=91!s zXs4GRJd7$_fY|^GZe9Z3Ya0C69WA_?{UXxif5L*|L7F%la*}W-GopqsR&pQXMVq*q zuk(c|(qZf%lPg18JGoOpA8s`x_c5yuDuoqLar9@fP(H1Sc5jmPA~R7?af85oMeV(O}6bU3n=&Zs; za6V!H`ZWE<<0GCAG-Fyvlk>IkIcaUt+oPlv$pRE0OkZ5muTw=7!c3ogJ8&AmS0J~} z9>o{73>z#HOJCNaN-eyW%daj6kDm+sRTtzFx&}eovD(YKYUV*tQ`(c(LR6Y(5hA%l z)@kA471nWL+2%+X$wG79RLhAZKmsqj*Ywz6b@>V!57p9^M7Z&!=Fnqd6OG|7~38Q zzXsqnDpNxvDO;ri`AUW|;>hKa#%_cEl2jL>t-EpURpX7TAGk9gtY!`X$&*Q!=!vSK zXg~{)L_?&Cd0cDgoUO|!r4SZO`ICVhPzc}}W#%xqW%LrW?K~muhxJVvAcluoDsVOB zZC*AINaA8X_{!^VW5CZo|BA>OIQ-Ia7~L|bSpULfn>aeqpYZGolw2(deZQ1WS*8t3 zW#p3!&z-l9Unx}WX8XpHz*J>ylVKfAP5$)9k}9>Rg}#VX5Wa+jZIDrMvHdP3Fe&FL z>U1bX3UDlRatW4aEBTps)Yi;)!1{sVMLd0PJK&<_p81wMn1#18tZn(*O4(Emb8~&E zM16f@RB3}vy!7 zW~10?=zBv0oSvXmV64Iq?VCd_D;uA4eP)M(IjqlMFsLftd$X%hatz&)16WinV%_5o zt7=NT)@ezEhX_nDnoQb;lS_D#0`Fwqo?)sPX%E7wV4JvNlmia4aVf@V>gSmYWk@aV z^dTh7?DQx+z)wz@?=+_Up=TdHT1`8V>qXr_5iup8=6O|+!}GDs-lM6}M|{#1vJ~9Q zv0jM%*kFU#ih@sTg3sEr{V`Q(yi{#?Q_&mLycicYZg<`IXSVx(M<;d7|3L$-Di)@s zP<|$ms9qIT?Jj^JRXtQ}Dbv(}pafsJ7wTaAF%L?`8l?G=^?4XNFXZpHNH7}u4M=sz zGzfQ8PcpWpW?>C>qxMNYNJL5Ly3-y?CsNg&INu? z%Vn#ZSx_6{xxfLq#XiD8C#Gu=gq|fmIeiM2Ak!pv0;)rcGWW3f6fo6id8H0SqZUZf z#j@&by?-LM{p*kGIVVUn-{8IhrT-2jv0w58sGp}fvJ3d*n5!GG*yE+QH0ad$A&9pG z^k4b(ko&&1uI6o5S>}FM+JbcULenUAKNG#0W<@$2(Vvp9eF`!&U3?K>pXK=V7hSFW zrIWkwRA&ybh4e)hp4s8E{fPeP?Bf6urFh6uAGh;yapd9Q1iaVTJJ~CETwHl#`n0mz zA+x(4uU9wO!$O;51PIC1ZK}K_LV8aBD)YI8Fn37-Q6jE|V_mh`7=FdDzp=tEw^Wzp zmx@Qh=HBPSRC-Y~1SCks$$NYQhVNjCJynA7C5A@}_ z-~p=)VYog`Wy6@dS%bXl1^uyg1#Mc>-CzvNPb@HNueBaE(?`&eTh5IvfcwXl0qV~k z^X56oW$d>Yn1Yr|7cnjLv#>??CBj)3x#^uITU5EUQI((VJjL2=PY;wHZX(XqpF0KE z>2?cNgrBzbTVKOFEmuktU}+rQ=$Jq}7i(yz-tc2}(+!?Rj6rtY#%=drMl+I?7B_jb znfJEQ+{la#E4JkdXd>@~H(YCh`YVJ?;0JZ7JjEB)jEg@^4W=x9uI1v#>}NFVOSS?& z&vf232mU)U|IBnV;bF|Cs%#N818aG!cqnyBhB@Szh=qHaVetu@NURTIAqa_$!Eu?Y zA?F{m6t?UWu9?nQQz)-YKo&%kT8Zy`7449x)$a_lFTVZDB~-vEdh_4^Py_#hFaYd- z!GZtMQ6KM7Zt`EVfI4R$_~m>!g~ z{V}s2%(o8od{9``zg-*P3yF3QR#LISj^jn2mwd_35;z$)5d!+u*E)&Tfpd+y;WjKh zYUsi^Z;TeKn&6Yn1?mthD2wYfXB-9v$y_qE2e|t6-vF)=*u-~HX99zmklVHK;cP~+ z8#l^l9qFaZG^3EwA!6~3`4Tv3lzi|P%OYN3u+KmAIWWMI-KH`T1GXxX)bluDOK6w+ z0@&ZzN%h&f$)`x#YDhL$3YaBB_0fQi0OZq9F$#xdtB5w3j&A^mKmgl&z=t8#f@ zC$P$%+?swH#*j@#?8wfvL4_^d@OHGg@@fXk#|DAMswwdsU_UQT_ZzBvkB-eh84o=l z1U&gUTtxwLO@SGI1&_P}Pw#`dam-4u+D5&IrUeKQ9ipPj*BjYKuMP$s}@$5m>=poAw*Jx$04z+DkcmA zC;=!e`QVX5l{6M7$;DMMlJzqTWGU%x0;;PH>4Cul=O zm`vIoBG=cl7qj)IiWl6d=ei~sSst5$tD*^0a>F4P|3H@|dXEe+y#oh80 z)HF)!UHgvTC)~8{7!TXD=bK3BLrS35WhzW^CeIS=U1Om0((x&gRLam6;uIhGum?r zNLe?Pa|b1{Dvz?F3~D9mhYjSs_^G>Ggsu#O?P`L?U?MnDc;WE821%S6ZvsRnOjuXh zMTmkMxe5(3I|N6}MnlMuRgkGFmNV$rj^th|E;7*>&kit&v%9dy;U34PHA#C_~k;X9<)Rw7(Xm zH9_sS3Sm<<`)KN}x=V2v{F-dg(SG`e!}ip)V4cVseF0pCFTh$qXi2Y&g3t<&U{{;6 zfk*=lN3r4a$D9fpXjw%#(!ONIcF{}~|SJw##@+vn>-4qp?K#RwVMnuDMZE~#wuQl&AV&OcuprM$vH zrQEO9^t@)PU08k5j~6k9O3(|j8Rw*oom}b^FuybWMgFDJnu4ipPg|&XW+?TwO%9R2 zlfbpq#6S&*jMRV`vu5RNU3ch71eZ5j6mhlApptenqOlOuU!QPpeG1RAH<+{U!qww?^zG3F?92(jfbFiH=UvcnOFvIGs(_E(F9L!B0v?t`sI zQmrERl0#hk24r)4iCVGp0VJwuMM&axnaPsbiYv(Bgf2QHQI)bluc8TXGLb6xp?!mt zO{K!NwawD1m!q_xg0E4t(RW{ciK<#d)0y2wtNzeXCfawX2(?6NDvUci^}UC=IN%dJ z(QP{;-_iA>?FFq9HrW8|!e@R578Z9E35g3*qK`2>usIX+*3VbGA_$oFa#FRcQl+am z2`UE>Dbj5{Cs}kP)&Vpyx)Sp$YW5=;5(|FUH6dv-C*=pzV=#aq_>saM!T~lQCI#)h z{;?o~Pu%*|LE6KN+h(7j0~jBx|Hpl-kN?G=E}Qul@b{5PGaAu@>POS0ky$cwvHf=XZoZ{=^Gx z#+V{MT|LSL_5M)JN}9V*HLX{!J=u5FoS}DUVJw1ArmwX;WF?F!*i7X20C` z>(N4?y5rSi&F~0Bn3MIQM@a`@4l@{|muVV%gGi?$8Hc8;LUbw~LU3h>w=G={vi2ZAJd8;pviy%z+}$p}X4|FCU+Qf@x!#x$)D-RNAQp}PqdmwG{>R&f zneZm1U(Dg>Jl%j2nFfE>DoQr{n^qBjs|-4mt)ex~*MQx5zV|U>#s1RvBPh67jnK8a z#v}kkVn;=RvbzKayi$C_Iwv-g@{ibCGTwd99Z{b%`kpb{*+JW$@Rb8r%@9Px=)WtZ zzDdwvSV#4LdvrKt0}?1@AzdGs|A`+{-o}NblxJ*MQQryCX%RmSuMi=Br+CrE3Lk~6 zc?x2rIc1Gy=SB_@nW*Q2^fEmB3OmM5dQQf3TGq?6X# zMA{j?0s2-ntJG7YKph8gQVwVpgyQ}wQ<$%Ts0D6&gb`e|TejY5JK4dV5`nB{yo^(i@S+GAvj)z6lnZ()6Q^Th^1xCU^$+Q~<3lyK3gip%yHJBtbCF`j z@j^hFk5hPk)=kTueTD`Z8YK>AofRKac~6rHo#2>=tiGYQi1QnqV9ZdQ`!hiYVsehhfzFPao!wKQT3Z4vpy`p296vx zgkM@q2uy*J#8n%6ZCov7dh!&;t`=E*!iPwSQfYRB=!~pEtU`&}S+`Xya8#?Gke=69 z;tS(FTAZIv7D&3^(q>yyw!jVwvZHX?xs;>4(HW%KKrCW*V%h2_KWkd8=5$D!_D<3W6x1~iYRjUZC zeqjw<%ej;I%I_>S_xF)ZO>Q!(cC`%kmrQoQFy~9w!sws1^cWhG7 zaQCSL#+uq44t_;@%aRfRgp?tkB+P!A1-@ttr*Hx^KU(&#H^~0bq8aX0Q}6LsZLvuy zV06P@8fHy^&J~A2FAB-Jn@-7uxzZu2GMI2v_%(KQ0+|=I<_1x&W!?22B_7(W1A|JI z(Br4XApT<8RwF=-K(V{)5K-e`<)e|+D}it_zjr2$2v#*K_Fmo2EOe;aLz&13Z)1`M>TbLJJlP-DNjdRGN(AN z8JEBZQPZyq=O2`VCU7}vrN+^^^@Hgrejc*UjYxH|dZoPMk2+c+ z4|?5Y_0Fd70eArR9FaD7?JKwg-lktcf{g-D)#zN%Z;y&)!%UBww~`W6O1#GKj0Y=K z4<8Y8-`+CD9f-n*xOp-25rkfV)+f#cjT>sG z!ZlF_-HwCi*1K0VpFsAli7th_{Eu{plU% z77dRme&Vz)i1LtcDr@C)%ag)qF+yql&}LoYNm{plVnU`Rm?S}H!seo*>{3VNv;2Mj zR9|bsn^+n9_vF=4_*EDZWsZ`0L8C37^iX>Q-_`-I;s$8FWHI*zW4<>fwkfrc{Urz6 z2m7whFvifvhAfqZ*>!cU;-gz+B|9AgZC{T;;d4V!kcZ_2<91l|8)!-8gJgK4C0}d9 zCrBA~$J##y3doncrX#K`?cg>Q&r_{x$Jz)9Oj7Mte4-N)S7LgDDh15HFV(0>uc`Tw zS_;!T1E>O>oD1;v$4a9<<}6EDhDt&C4aq!GGlf*dS+RT4nv8%5Q_`3lCM)dk!xUYI@7TK@-d1?r_!Q zcPx@kKoJe)kRQH^`SrfY-Pmf}SkHHA5M^9fVffS&<(B3pwBhV{QDYN*fOp!(E%pLQ zrznPDYJ;Z=pG|OTyWN%1E@Qykf`gt(dE;mSNy8^uWD6A;)9Rd5L{a(G5UuIp%nD=>*#cjZQTSj5qoO*L zHr1q4lzGE^3cNcPV zXmAff8?>N{V4Sjo+nFIAwd;8Bkc{VM0?voWbY`mAd=1*`ARyC?R;A!9cUW zpRnRduoB};@)EF6CZ)sZp~c~lkp?bP)Hbd0}l~Pw(!Qj}#DhQll zz*f75379R;Yhj}Km2Oz@v6_L|Q!du=Jb*wi{l4fY%`&NbZ^v|g{#t=$A-!PtE1K3T zWGB>^$oO;rEglWR&rnI5oDHSeQzc}s`P+SHEoI{U&lxgXv6kz81UvMG zX7%An)zwPaa5CJxUQ*k>D%V&asoaGfx?2WY=~mbWO|U6DU6i-(E{Vvrh0)t^TUp|k zzhs?;I(^|ULGzuc$q4B*G)j2Be&H!JDo3g4O{2>l_fw$A5L7a;A4zOHYb~H}WTz!% z=DdYaWt#6>Uf`_c??z}`U5_w#I_1tl`{A3fA+?R%w}{E+BFytJZ2Sd8$#bN` z%t@!E@;`@_Pz^scv7_$>qaRbf{3WJBQ>C2h0-kxg`{WgxZoSMV(qcd`J8lCeeMjFM zC#dpbtO|;$@~KpVlZ>CE;v2Zos-RWQ5Pk-vny0x7!glcF4np|`ytY!;Hcdg$qI}?N z9`lB!yTmwM#yOycf}hw_;F!seol}VxSQ;bfNM)gs+A|CYh~f+kcwVF`e2IM(5$NTo zCVbLn%vqV1AL8FY+oKiKwW9A3&7^z6sl3;3A?(^-qqnq1F$kH$uX-=Jd;^Aj_y&d0 z(AaA`B0X$^?iu9MHLbzan~rp=(8O(;^yNU_hRePIu1~8){sD_XQ1P9>$jJ5QBg9Xl z)cL(39O(8G%#Z2uA?gX0kV;pLzqUBe-VyW3%_fHp`oVsyhi(`G!ms|Aa6g9FlU+y_ zn|O4SyyoGT>oF4vf0s-bXw-qS&)IKCT=Ci#-0mxG4YL0Q!KI{;UVAlk01!b)oB`;J z6BDRr$nWsEXgcdd^rl<~?d9#7jXV#o9jE2J!^6%`MO;iNB3O8(l}i+&gf0IAyu z*7PX5SM?t-sSPSlu$#K*&LC~JC{u8eb5{kgjB-n{>vW)oxFZPbV)r6<{LBfmKhg+E z8NA3zR5uQcG}L!AvDG3V!D?;%q!5gg3@AbU!+LDlO!NLC*BQ6kC#rhw`iz;OslSW` z5VY_J(ix8{Ss*A%sK@xX9o~frXI^SJ(%LwSSij3j>OA`Mxz{GO#l0EbdUG31@ndOj+<Nw@@BM8Yh}v~Qv}tQ_ z?=vInzMtdh268}(dU^mLOp=)@F-v+`j=!TDi!ticpFkcF<*yUU*lx1OWQH1 zGkh8fXA0$`$%?CwlO3uB-!yFbT?h`%t2}aO?~KwceknX?FsLN1rqU;ydVvHYb7iK< z8NfeNlRKn!i*WK)^-|?CH1M?Fo<%&EitWshe~2-8SsT3u=bux<_R}KpWbX|83MZTw z0|6OGsYuf|tZmhYTa~HBHNZir2+Yzz0)~x)eqizp6)jJAfYtLgbjnL%KI!`NSoi52 zC0a1#=-?Qt)j0YV#cMcWq(LHm7VJ{^@f8r>Th(tK>IeR(9Lt3KuLI{F2?hc7uL%e2 z9|i^h0R?~|gQE~bFrkwCw<^mbj7G{_5802d6!^cbT_7L=09##FrI5J%zMv{4rw~g& z1+Rf%CA8eT1gYAbhJ}fcO%NNn;iX~~e=G-b^>2VeFu{lds@zmjeO$?+rhViXtOQl* zO1a`7Wt4*=C@iLgZty{PRyky0XLqz7o&k#S8Lpz% zYorwGS*ea4z0Z`I>Ln8_p_1>77GO{u_mLBc=!!7R#*-${UtV(ui8}Ezu(9hE&bEeYI#k5=J|6oK)5ROzo}q{wr3F5Z(iD z3Fn)PRyc!EE;E52tY;xs^W{c7(ri>N>!YY@pQF3dn~o{S(fis){>F?`Sjy>mW~gJw ziiQ%97HjLHmC{X_LDC?x+l_E`w^Yrn&9Mh4#Xn};htAnUOab@M<0zR?Z~)T|J7zUz z+bI}qiSd-vOFQB84QYkf7v0+<98f`AftWW(k-1|e7?EAFh7*c^68j&ykmjPsp512)6b_IYq@#@ah2IP&H)l6Liv5uV^P1UuO19o@63}aX! zJ|N(>aiF>DoV987OFdO44IKyKq6(jqQ4jC_Lmf;bJJO+luzifaDh z%gTT^)`hPLCqi8Ls+)lF3sIOF1P78=te`C^C?Zp*KWf!mCG%$)KF&jF_7__?%i<<*A)F%^QE5bK3Wfan zO7B?QDgJw@sSKjsK&a#k>UP0qF8pw0LP=$~J_k-if@%K4K0LR+8RH8~c}=ZVaDQg6 z$tqXjpX0`VEU0f?*beHr-l2G;<)1dN7DazGbnW4;6>7?fkog?aEdkcx3&LmcN0-Op;7`d%M!{` zZt1OyAL18Ecw=fM!0_legrlMbX+w zC(Pwe8Onw;ql6GWvgKCEE^yWTkS=mbW-%X9cT26tS2tz>E-gJ1=v8kw zm8|%5R$s@3{Vust(IlVU|0jS5fA?b&W#_|CzNQGRi0f6Phecp!ul_zPCG@Tbx;g&< zMrwLyx`G4H{{T=o56GGxp6No1yuSeC%BiZ6n17r{~BvE$H#>|($|0>{!j7i^y7{(!=8<|{#DRt7VH3Cwaf8PW9WmeqZug5W6l z&n9ZBD`OJGocnqn!gU2k(mwK~c*F=*w*DpUgRpjA_b~<`AVtG80~0V33~$lN=1_{a zr~?6T2gVFbY_iQx<|h_legx`d?bEJ!DoGz8%RmKw21ndZ#bA(R8bF+flNf@XjJ z!~ixC00IF60|EpD1O)^G000000RjU61Q7)iAu&M{B2i%m6f$uTfswHlBSKQ4Btvq+ zg5mN1+5iXv0|5a)0Jwr#F697oyym__LOWCzA|hC~H7K2*fN@*iF_m1JSPa)2nQ@a- z!j>+t81rNr)GnpI*w58PTWVMK@pv&x!F!4?CEdFhIUv!6kk^F9ti-X+5kNR`R%}h2 z1dJ0B=k%_w)v7=(@1{iRTAoLUaw))*Y=Gjnnj9bfQ^FV=jPcT^wYZJ5 z5Xzq2#T30$%yn&Tc_qv#w0W3!t99`0;!i|T`U?J|TJ@x2@^{G2X<~$?;z!=4NPru8 ztv0|&d8jPy;#>q2V1c^CzojjWqXNDV#T_ooAcPqeabXIKtU%O|#@uY$YUbPkSPH(1 zYiY2qcDEy%w3=w{VqoVX#X!ZaqCT;&?Oj;c#+P#c01_7RRMp?2R}ifm5B)HD1GHr*pdkPo`=sYiUpNXBqe6 zt=@RRS$Qkbs(X1g3xth-x0C5wr^CynKACc4G9;1}In!E_n>ijfFB8aA-XKxN(PPg?mH)Pj+!`;kz=;lx2d zRw<(KBir_=EU?FND}n7xWeLtB9Q#v15m^a1#{#swW+q7owQDBYTP-AG4BSVRDm}Ulh{+pNhO$usHKJF8Fmc4 z%}aF(up`Zy1WLKvwEgL24dj!rV^XSw03Ui+Xl%{G0NXb)8T6|=%bE2QmQ0fJWxqNE zZC}cdTGt{^;xn9gsKNyWC=WS%`3B^7$lR@kcLo2U9^DtPC8R7IO0s6^=lkT z4(8^ijuXH+KGbB65O+#fCmiumxspsPZW$cZyhh%_rhV%8k$`sPazHg_7y8(6c->3% zkFc)wtwP^5IX%ror^zh)iQrWB%2{2H(^_QfJ+ff(2H(=I5K0##gOF-{B67@arrVL` zQgAALGg@a1xtGpJDA~a^9+RuG%Ff#h<2#=j=h~A>W|w`$hC&#SJ7m;xBy2W}1r@2x zBDQ{c#y>MaJ2pw+RiclYh=bfx{VPq@z9YGiUC67~HE(e|aWQUbXO`hP6{2P)nm)%2 zKkkVK_o)W*3h|nPSF_#xvUhva+)HuNGCj?5sxv{hg4L)@E*L0ms*{uNTTn3qqmZS< zIOEPLB1$LxMK@a46(zrr6Ua4!Jd`)Y2wrPUy=1?MTm8(X7Yj==fgyj(X3O#G$O~+7_d(+0L z8aV4h_^NqETBl}q1&&EHuJF3a8x>_a1Xho%$A2G&_*s{z_4gI1wS~IxxOG9>yEodk z4P~dycBo;Gp={;b^X*D8R>{4$o`a{&{541KzV7FnF*!_fczhsN3g4xEUt^$f6ZII=)h0_#abNP zDuCU=G}r{yvSD|gty!a-)(sFfOG#Z1S*9QEYWDy(qeV=5}otfr!7GMt{c Rq0{j0Y9J|zB+&dn|JfKbE7AY} literal 0 HcmV?d00001 diff --git a/tests/system/Vision/fixtures/text.jpg b/tests/system/Vision/fixtures/text.jpg new file mode 100644 index 0000000000000000000000000000000000000000..55f1db964e89ec0369d69ab00d1990d5a85fec6f GIT binary patch literal 7842 zcmeI0c{r5q+sCgl7>4ZG8D&kDFqD0iT_VLJLLp0HvhNI8vZum>G9#q2WtS{5G9gRZ z*Rd7~GlfQmc|E`1d%VZ{*Lys__n-HV_jlgMeP4gv*Kyp}=RB|LywCGXo1o1D4pT!D zLjZvQ06Dqfx%#KCO9`7&MhOzD=70X8|@w7WC9-`51|9)Lxw(6I-u4d&3rD=IE| zUi#wYtD4%n`i91)H_e^+uJ_%94?Vq~het-o#wRAH78aM5SH65*U0Wyr__@8a`-`&o z8v>yJ#5sEY6X?J39DzgV=;@*Ku)la9biqeAl#`y}gc2i{-euTLU+$C2F-$z?GYhLb zn8j7BNXMQ1KEZhb|5rr+1@vD$v?;&}g&YM8$_aGA@8=jekO4Tl ze-Hn5!M}IG|8x+b%F|tf24iphpG~g^`K^!jd@*->cu&rYAlHi8%lXLzDUwU#K})1^ za`3uI1IWk_!!g+P&n6fr6R#CGhH^>0hdo|JnTJ(=_ zm6ojsX{bF_J*)_3c>4I9vV~@xeh+XhfJv}-Vb`uj(s&Uv^EWmTU82kJ}aHROrLp5A*jZLkv&dt%tYGHS7|D@sIhkRS3Rb8q6p2q*yD9T{(;_4S)rT0q{WjDf!(~d+mESRq0#R?=cCqB`@(v-zsWXgW&!7K0+J+Vv!?+YWg z1yhN?;g$?3#I&5>?f0vYRS9kEZ^`;W7p)fe2RTsuK{jIdi1e9Hgz40A$J*nGyw2o= zH<1goUIxu18c6#7xJNRrV@3obTy{frem*^p!?*_sqZ8Yge?*wkz>Po}7!t$5rfHyO zd4>iic%oNXP$G%%EmE@ESVqgYMNXT(8K+u=Kn+T#g}tppViba9#uTVye`hn(B z)|Z^c6nVZyqc|U`;_eTI`e}==`X=Z0(uY6~cyLKs8GONLw_(migcnvNMI5j6=pWl1 zu)X?Q=}O%pj1i+n*fkkvD@@mWnZMHQO-WQ^Xb(Va&^7TW|ys+=k9#ng)r zFC1i*homo=TZJ48=<-D|!xBEOZ?(l~K>@b0Rconib zr%ZN?Csn=M{iTr^SX-YaFYmnoTFUw0$r=IImq`G}u9Uyjm|VA^Ky(<_;a^uP5XwsU zR`Nz}HL>Wp%o%p)X}u3$^T19M_15z_sxJ2ZluG1V3$s&baqwNS+C;`r=Sr3sxdX>8 z@%o_T&W^V1WF4B>OR%6`EI6-oSec1K3FOkbF5e&(v(D!MJf*(8ZxJC#WgD%OyEXjf zZOc!`&sJvVFAm)@5%bfD#b{#}+q)tW3k{gOgmQ_3&B`}*ff8^) zM&BnMZ@Kj<;@sGc(L;gTVL|yXChNRHzp5}@i000ayWuh}(&_;nr2%)vy^{sJG*A^x z1D~o;qM3gk5MT-U+XJkRWxkb#pj-b(THO}6o$73{jqkwyTCEUYktUd|-fA~So5kh; zt0OeM7W~Md(6q*gr5W3J>F)W5>_i-!(QbIQt*)B|kD1T+M`G=+x7L~BZX|>518jMl zG&W)EVTkC@PL4HYi__k%*CJ>@B}gc0M67^OegCZow2|}`-+uoO1b-b`Fr;@M@6%wj zb?4jjTt}FWmv6xdK;fEH|UG2Ltrvo}Th-{}8m=7Mr7#(8jAPD^ZdF*aE z<(}#^x+P(hq3zJL@+3_3i8tcH9_CKe_-lJJQc$P@mkRRi10*XU%h^~nayOf*vW5~Z zo$ry%>G&<2!;t4*9rL8AL(b*;a@ZG)nhbQ=lZwK^sc3SFQE2vLAd6bdaHBBv^uf@* zHwR>@xXxq9r;xWD$2|AN*Y4su?GM!W17ivVMDioB_pb^4z%hD;^+s48;3ByFxr-a@ zbdp3%WYf=I`qq^3!ae114|mLB%KBy#C7)C#^|6Au&!A&;HE1mm-^-0``_I{IE0%OYPUx>7svK( z_hLgVBKEiI5T3{1LK|AF9RBOHzr$6EBZAQ(J&(!QJ=Vt?35qyfWL z5N7bN?J)>S)PA8VF|RU*OzfIdZEC9aR=iSYAL4A}ul$-L^|dSy`)s43wgCvUDS{^2;3qLg2hvXB!|w$gi?SDbS^Os3W?x06`*LThNN;KNAP=N-nkqpi zVpRLy9CB0A-y_X>SZh4lRl&67|QRKwHFA>U6Ame&Uior ze5E{KwGFYJMBYUU3#BN2FC9MoaAIyQiVAt+J)N%GO0i^gdXIauTaX$*OG7(O<4i6ia}C7dms2e zECrc>2VlShx=L@Yf7Jl~>I~oqmUKg~IBEdp)QgDoF1Am$yYeU97xlk;-92|8gr< z9qi)Rkt14sWW3`LH{Onr6u=+QqZ=3)$?Wfobbvp4?Vj<=q?>tp89H1?Ox$gTb*8j4 zuCl>$U9_7A)d{(B$?x9Iix%&~py?w3OHVx!J@1rg7Zg)w?xni&ws&B+ykCGQex~Er zm7(+zNCw$y;Y-;f4Tywr+YS53IoA*Q^uO;N*c2K6nDiQRihhCJO4E`q!jIzQyEK7p zHPahm%wKdk@i9A?)!1fPq&WR*EQA$8(DHu=W2vN`qx3B~z?UwPEmD#BUV2O{46is8 zibd$lUlgC(O3m2yfR0D;QJjBvMjxxgvOD$D=QKP(q9s_nmEZqKGKLBNhJ%%McN$2X zlOVBS%P%ZeSWR1IZ`w|AGj!F&SZmNZWOi*0vKr#>)V$S)M}fDnbWoGK~di$-|wvxXyrQv~ZaB}aX6LvF=< zrVa_Nb8c(tb&&6d9&yjaWkX9TXIYOb1Id4;DN(bVWEQ)`-Pl%=PPNFxHLIz}B8yD% zTUi66ca-Gx)?`+7Hh}rQeuP0A#dm>4b5>B(Q&p&}8ZO_-i;B7S=R8Na%(pp=1~I`= z0?kK(>5ofrCT(tvmfCh7zlAuBVMp<+0zU+^R&+Uq*nOzln0;EGbeQ44Yj2dUERosx zy2raqlv@k(D;|VBsUg4O0 zJ<(^jT-&1RHrQ$YTZJ$5D{=b#505s)>X`3aiL-WjNhsYAStaP&6T~}9Si|J|n+sSL z17W56$XT_ao0jf(d`nBC7(i$S6&a0Bjs9@yCk=pN8i0e6ePc?MZR~jG951PUc+!zl zu#EC~oaLG!(*%3blj&!4jy@R2TH&?pIdmsN)|E6@^m00+pr4hr&KME;Fy;w|QXThD zCDZ)!{4(8m%%S}HSwwC6%f+&-?KCeFcDK*UWVtt&g6MYF1UpLZ}NSj0Ja z4x9S*AbNg;AFELmfm}J-ZT%imqHib%WbUrKjHG&@i5BT1hsf-K&N=hK`x4Oa#sV&> zPv_~tJQP8o0oHA&L|wi^!3_5Ph@snp&r~k>i}?ze+}uwDKbyX)Pk+zh(nXIS%~VYuT{Dm9rG=qS|Yr4ZD?L!tHWDuH!zw8xLO6 zz?~ORq73vac1ksRfuE8?+9D`nGVuX&K1Z$0^Uwj^t?WDuk{ZnFc;=v}B(txV3x4HUXm|DnPjVRlDc+APV zJ?ByZbK19rKja{Cn)iPElESECCo^m_@Wk3LwdEJ%mtGfat~`k5FdnV`ZOO>qIT&BG zxcO8uy-i|0xUpuP?VG1J{aY>xo6}n{4^Wpepl2WiI32ZoH6+CiGqt4W*Jo$!2*tHU zv*$d4Y`X#q3gs6Lz_DR>%M8!zvP-c1P-sZDD3tW2rh)K{zg3JuzBttx#Z*Rwk`}-8 zT{Qn)@}w-(OBD4}K#xH=Jj;#HCId~Q{6YSC9&40%o)ROGr`GBgI~CND6z8ACFYzQI zjqs5lQjog}&b3c@5p)>IigEHtG}FwK<9g?zy07PL{L0if{04Yv%Sz_~tvV?FUN0}N z1zqwI3iXkvYWh_Lb3xx&r+PnH)eX8z11&PpV)9u65o#YI<$c9s)TQ-po|G+e$4W|> z<8Ed9JIFdAHDJIHbuI%?)yJ^Ae~fJ>#Mt zh=s*d!r{5IRyDR#yk~pn8Aj=lBLshmV!hNZ7-G@=qaja0acXmHQy(qgF`$wjlj8|1 zwY=nu5@_dcJ4uNqUI=%niI`Oiny*LZ*lhZ^TCDamu@b{jHeF4%=Ry@+cfKT zMaSo57oJET`254<%iWUXXMGT9=%aTVC=vGeis_ED_*Rt)#!P9v)-9$ z-uQ@#sXI2=(V-3g3VR4?bSu7%YqdeJ*!%eRm`&HD4n|$26b7+vG0x11FBlz9N=k7^ zVyBJ{nGavZhCyQa9PIn)I6`b5i7Bx0FwRtT^x|;rH4&!@rzl?dFrSngO5QGXsKmE7 z{0K8o9&LI+Z7^C@yHAFq8>85~?0Jy85cHCCV|G+D^=>P;ORR}&oRb-Cxw7PNqiA+U zzTruEGD`Hi%UuEK*Y~jzL$HFwPp4mm1TUhK>b#Yh8^~F$#c?v~b2R|`ZWi9x29aJx z9+y1}*lLGvXgW7m+pD5)2C-A57?aISrSv4 z_d82>m+59Qh@5MMy|0=&jb~plc(`Oh`k-=s9Re>>GkG`YY<)FYS4o(4h_N)829ER& z@uRjS`$+Fl@PN+N6ZhzDBX+i=4`YwMH8{CsobPmy7hzOv_n?t)FdQx5lb&35a_0f%<2~ zOBUX$^GRPEo|`A*8Z@C3qQ0q1B_J$=fS~9XI5Cx`RtCzLuD_H|o_ulAB!~Z+a_Ne? zh|&&@{qo;cLh<9&6AObJy*1yO*>Ya=7O_b7zcQW=V-!D$;npKXCs1`yn-#8VIAT>; z1>`HN&Wg`s`hoX{6p1?zWt-2)_Tt6vO(cVr@}}d2SA+ zpA&D;u!Q}A5+10XM=*N@Z$*|-(8et-5$FOhefNmykR@p{?$|zyri0FakQs}IxAzIl z%nNXO{mVlP!Vr`|)JalKVv8xM{XWuM(<(%dvvsuj!v}UtW&!9Fh6}+I;=}Zv3&Fm| z)Eu5G_pUZI@@W!%T|`9j12#i818{AWs$1lxGHqV&sS!yX>-tl4eNxgRsXHhehSnP% zvSf73C+&Cqb}JM)e7X?HJM}}QS^ZgU+BNo956M+>abOH`h+Uqu=23bQ^) z#}=1P)Wx;LEY9iN`pI)ffPap74ORle`;TNpk&m1X_I(T=FZa{GvNB$_>|Hlx5+mt} zyr8R0EAq?}DXjk#@J(K0q}u;jF$Z z8cZbO?mf8tCszw5WD6x207+a_WTFIjRdG}9=?~Wnoa9g2^d2+RTm6S86g=#tvT)Z} zwAnA!4R83r?QMv6v5GW?KH5UjH%n!y)v{G7;%~{I#i@rJfCzDa{qbb>=}u+D>M)q46WNw2JL@aPx|}H)4yl_3o_mRTW3YnCjSGC CVmy8T literal 0 HcmV?d00001 diff --git a/tests/unit/Vision/Annotation/CropHintTest.php b/tests/unit/Vision/Annotation/CropHintTest.php new file mode 100644 index 000000000000..971a9198a628 --- /dev/null +++ b/tests/unit/Vision/Annotation/CropHintTest.php @@ -0,0 +1,55 @@ +info = [ + 'boundingPoly' => ['foo' => 'bar'], + 'confidence' => 0.4, + 'importanceFraction' => 0.1 + ]; + + $this->hint = new CropHint($this->info); + } + + public function testBoundingPoly() + { + $this->assertEquals($this->info['boundingPoly'], $this->hint->boundingPoly()); + } + + public function testConfidence() + { + $this->assertEquals($this->info['confidence'], $this->hint->confidence()); + } + + public function testImportanceFraction() + { + $this->assertEquals($this->info['importanceFraction'], $this->hint->importanceFraction()); + } +} diff --git a/tests/unit/Vision/Annotation/DocumentTest.php b/tests/unit/Vision/Annotation/DocumentTest.php new file mode 100644 index 000000000000..6e6ece9658c4 --- /dev/null +++ b/tests/unit/Vision/Annotation/DocumentTest.php @@ -0,0 +1,38 @@ + 'bar' + ]; + + $e = new Document($res); + + $this->assertEquals($res, $e->info()); + $this->assertEquals('bar', $e->foo()); + } +} diff --git a/tests/unit/Vision/Annotation/Web/WebEntityTest.php b/tests/unit/Vision/Annotation/Web/WebEntityTest.php new file mode 100644 index 000000000000..2ea3269ea43a --- /dev/null +++ b/tests/unit/Vision/Annotation/Web/WebEntityTest.php @@ -0,0 +1,54 @@ +info = [ + 'entityId' => 'foo', + 'score' => 1, + 'description' => 'bar' + ]; + $this->entity = new WebEntity($this->info); + } + + public function testEntityId() + { + $this->assertEquals($this->info['entityId'], $this->entity->entityId()); + } + + public function testScore() + { + $this->assertEquals($this->info['score'], $this->entity->score()); + } + + public function testDescription() + { + $this->assertEquals($this->info['description'], $this->entity->description()); + } +} diff --git a/tests/unit/Vision/Annotation/Web/WebImageTest.php b/tests/unit/Vision/Annotation/Web/WebImageTest.php new file mode 100644 index 000000000000..f5da1e1de743 --- /dev/null +++ b/tests/unit/Vision/Annotation/Web/WebImageTest.php @@ -0,0 +1,48 @@ +info = [ + 'url' => 'http://foo.bar/bat', + 'score' => 0.4 + ]; + $this->image = new WebImage($this->info); + } + + public function testUrl() + { + $this->assertEquals($this->info['url'], $this->image->url()); + } + + public function testScore() + { + $this->assertEquals($this->info['score'], $this->image->score()); + } +} diff --git a/tests/unit/Vision/Annotation/Web/WebPageTest.php b/tests/unit/Vision/Annotation/Web/WebPageTest.php new file mode 100644 index 000000000000..488ad080ec8c --- /dev/null +++ b/tests/unit/Vision/Annotation/Web/WebPageTest.php @@ -0,0 +1,48 @@ +info = [ + 'url' => 'http://foo.bar/bat', + 'score' => 0.4 + ]; + $this->image = new WebPage($this->info); + } + + public function testUrl() + { + $this->assertEquals($this->info['url'], $this->image->url()); + } + + public function testScore() + { + $this->assertEquals($this->info['score'], $this->image->score()); + } +} diff --git a/tests/unit/Vision/Annotation/WebTest.php b/tests/unit/Vision/Annotation/WebTest.php new file mode 100644 index 000000000000..1b449043471f --- /dev/null +++ b/tests/unit/Vision/Annotation/WebTest.php @@ -0,0 +1,75 @@ +info = [ + 'webEntities' => [ + ['foo' => 'bar'] + ], + 'fullMatchingImages' => [ + ['foo' => 'bar'] + ], + 'partialMatchingImages' => [ + ['foo' => 'bar'] + ], + 'pagesWithMatchingImages' => [ + ['foo' => 'bar'] + ] + ]; + $this->annotation = new Web($this->info); + } + + public function testEntities() + { + $this->assertInstanceOf(WebEntity::class, $this->annotation->entities()[0]); + $this->assertEquals($this->info['webEntities'][0], $this->annotation->entities()[0]->info()); + } + + public function testMatchingImages() + { + $this->assertInstanceOf(WebImage::class, $this->annotation->matchingImages()[0]); + $this->assertEquals($this->info['fullMatchingImages'][0], $this->annotation->matchingImages()[0]->info()); + } + + public function testPartialMatchingImages() + { + $this->assertInstanceOf(WebImage::class, $this->annotation->partialMatchingImages()[0]); + $this->assertEquals($this->info['partialMatchingImages'][0], $this->annotation->partialMatchingImages()[0]->info()); + } + + public function testPages() + { + $this->assertInstanceOf(WebPage::class, $this->annotation->pages()[0]); + $this->assertEquals($this->info['pagesWithMatchingImages'][0], $this->annotation->pages()[0]->info()); + } +} diff --git a/tests/unit/Vision/AnnotationTest.php b/tests/unit/Vision/AnnotationTest.php index 746b2f003e30..b89b37b725c8 100644 --- a/tests/unit/Vision/AnnotationTest.php +++ b/tests/unit/Vision/AnnotationTest.php @@ -18,10 +18,13 @@ namespace Google\Cloud\Tests\Vision; use Google\Cloud\Vision\Annotation; +use Google\Cloud\Vision\Annotation\CropHint; +use Google\Cloud\Vision\Annotation\Document; use Google\Cloud\Vision\Annotation\Entity; use Google\Cloud\Vision\Annotation\Face; use Google\Cloud\Vision\Annotation\ImageProperties; use Google\Cloud\Vision\Annotation\SafeSearch; +use Google\Cloud\Vision\Annotation\Web; /** * @group vision @@ -40,7 +43,10 @@ public function testConstruct() 'textAnnotations' => ['foo' => ['bat' => 'bar']], 'safeSearchAnnotation' => ['foo' => ['bat' => 'bar']], 'imagePropertiesAnnotation' => ['foo' => ['bat' => 'bar']], - 'error' => ['foo' => ['bat' => 'bar']] + 'error' => ['foo' => ['bat' => 'bar']], + 'fullTextAnnotation' => ['foo' => 'bar'], + 'cropHintsAnnotation' => ['cropHints' => [['bat' => 'bar']]], + 'webDetection' => ['foo' => ['bat' => 'bar']], ]; $ann = new Annotation($res); @@ -53,6 +59,9 @@ public function testConstruct() $this->assertInstanceOf(SafeSearch::class, $ann->safeSearch()); $this->assertInstanceOf(ImageProperties::class, $ann->imageProperties()); $this->assertEquals($res['error'], $ann->error()); + $this->assertInstanceOf(Document::class, $ann->fullText()); + $this->assertInstanceOf(CropHint::class, $ann->cropHints()[0]); + $this->assertInstanceOf(Web::class, $ann->web()); $this->assertEquals($res, $ann->info()); } diff --git a/tests/unit/Vision/ImageTest.php b/tests/unit/Vision/ImageTest.php index 7ba69af573a8..c2b2fb948359 100644 --- a/tests/unit/Vision/ImageTest.php +++ b/tests/unit/Vision/ImageTest.php @@ -25,7 +25,7 @@ */ class ImageTest extends \PHPUnit_Framework_TestCase { - public function testWithBytes() + public function testWithString() { $bytes = file_get_contents(__DIR__ .'/../fixtures/vision/eiffel-tower.jpg'); $image = new Image($bytes, ['landmarks']); @@ -47,7 +47,7 @@ public function testWithStorage() $image = new Image($object, [ 'landmarks' ]); $res = $image->requestObject(); - $this->assertEquals($res['image']['source']['gcsImageUri'], $gcsUri); + $this->assertEquals($res['image']['source']['imageUri'], $gcsUri); $this->assertEquals($res['features'], [ ['type' => 'LANDMARK_DETECTION'] ]); } @@ -63,6 +63,17 @@ public function testWithResource() $this->assertEquals($res['features'], [ ['type' => 'LANDMARK_DETECTION'] ]); } + public function testWithExternalImage() + { + $externalUri = 'http://google.com/image.jpg'; + $image = new Image($externalUri, ['landmarks']); + + $res = $image->requestObject(); + + $this->assertEquals($res['image']['source']['imageUri'], $externalUri); + $this->assertEquals($res['features'], [ ['type' => 'LANDMARK_DETECTION'] ]); + } + /** * @expectedException InvalidArgumentException */ @@ -87,13 +98,16 @@ public function testMaxResults() public function testShortNamesMapping() { $names = [ - 'faces' => 'FACE_DETECTION', - 'landmarks' => 'LANDMARK_DETECTION', - 'logos' => 'LOGO_DETECTION', - 'labels' => 'LABEL_DETECTION', - 'text' => 'TEXT_DETECTION', - 'safeSearch' => 'SAFE_SEARCH_DETECTION', - 'imageProperties' => 'IMAGE_PROPERTIES' + 'faces' => 'FACE_DETECTION', + 'landmarks' => 'LANDMARK_DETECTION', + 'logos' => 'LOGO_DETECTION', + 'labels' => 'LABEL_DETECTION', + 'text' => 'TEXT_DETECTION', + 'document' => 'DOCUMENT_TEXT_DETECTION', + 'safeSearch' => 'SAFE_SEARCH_DETECTION', + 'imageProperties' => 'IMAGE_PROPERTIES', + 'crop' => 'CROP_HINTS', + 'web' => 'WEB_DETECTION' ]; $bytes = 'foo'; @@ -122,4 +136,26 @@ public function testBytesWithoutEncoding() $encodedRes = $image->requestObject(); $this->assertEquals($encodedRes['image']['content'], base64_encode($bytes)); } + + public function testUrlSchemes() + { + $urls = [ + 'http://foo.bar', + 'https://foo.bar', + 'gs://foo/bar', + 'ssh://foo/bar' + ]; + + $images = [ + new Image($urls[0], ['faces']), + new Image($urls[1], ['faces']), + new Image($urls[2], ['faces']), + new Image($urls[3], ['faces']), + ]; + + $this->assertEquals($urls[0], $images[0]->requestObject()['image']['source']['imageUri']); + $this->assertEquals($urls[1], $images[1]->requestObject()['image']['source']['imageUri']); + $this->assertEquals($urls[2], $images[2]->requestObject()['image']['source']['imageUri']); + $this->assertFalse(isset($images[3]->requestObject()['image']['source']['imageUri'])); + } } From 7b084082f89108f5e7575e0788a04bc3f60a0a07 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 2 Mar 2017 11:24:24 -0500 Subject: [PATCH 075/107] Revert "Remove rate limit retries" (#367) --- src/RequestWrapper.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/RequestWrapper.php b/src/RequestWrapper.php index 3e76cf9b9f2f..66d3e6579aae 100644 --- a/src/RequestWrapper.php +++ b/src/RequestWrapper.php @@ -64,6 +64,14 @@ class RequestWrapper 503 ]; + /** + * @var array + */ + private $httpRetryMessages = [ + 'rateLimitExceeded', + 'userRateLimitExceeded' + ]; + /** * @var bool $shouldSignRequest Whether to enable request signing. */ @@ -238,14 +246,27 @@ private function getExceptionMessage(\Exception $ex) private function getRetryFunction() { $httpRetryCodes = $this->httpRetryCodes; + $httpRetryMessages = $this->httpRetryMessages; - return function (\Exception $ex) use ($httpRetryCodes) { + return function (\Exception $ex) use ($httpRetryCodes, $httpRetryMessages) { $statusCode = $ex->getCode(); if (in_array($statusCode, $httpRetryCodes)) { return true; } + $message = json_decode($ex->getMessage(), true); + + if (!isset($message['error']['errors'])) { + return false; + } + + foreach ($message['error']['errors'] as $error) { + if (in_array($error['reason'], $httpRetryMessages)) { + return true; + } + } + return false; }; } From c3c4472948bdb8ad93fc35b9e3b2de2fca6c06d4 Mon Sep 17 00:00:00 2001 From: michaelbausor Date: Thu, 2 Mar 2017 12:01:52 -0800 Subject: [PATCH 076/107] Regenerate gapic code (#368) * Regenerate gapic code * Regenerate after fixing PR comments * Fix file parsing --- composer.json | 4 +- .../V1beta1/ErrorGroupServiceClient.php | 33 +- .../V1beta1/ErrorStatsServiceClient.php | 33 +- .../V1beta1/ReportErrorsServiceClient.php | 33 +- .../error_group_service_client_config.json | 14 +- .../error_stats_service_client_config.json | 14 +- .../report_errors_service_client_config.json | 14 +- src/Logging/V2/ConfigServiceV2Client.php | 33 +- src/Logging/V2/LoggingServiceV2Client.php | 33 +- src/Logging/V2/MetricsServiceV2Client.php | 33 +- .../config_service_v2_client_config.json | 14 +- .../logging_service_v2_client_config.json | 21 +- .../metrics_service_v2_client_config.json | 14 +- src/Monitoring/V3/GroupServiceClient.php | 33 +- src/Monitoring/V3/MetricServiceClient.php | 33 +- .../group_service_client_config.json | 14 +- .../metric_service_client_config.json | 14 +- src/PubSub/V1/PublisherClient.php | 33 +- src/PubSub/V1/SubscriberClient.php | 548 +++++++++++++++++- .../V1/resources/publisher_client_config.json | 22 +- .../resources/subscriber_client_config.json | 44 +- src/Speech/V1beta1/SpeechClient.php | 193 +++++- .../resources/speech_client_config.json | 19 +- 23 files changed, 1015 insertions(+), 231 deletions(-) diff --git a/composer.json b/composer.json index 8e2b924f3f15..4402d1145b28 100644 --- a/composer.json +++ b/composer.json @@ -55,8 +55,8 @@ "james-heinrich/getid3": "^1.9", "erusev/parsedown": "^1.6", "vierbergenlars/php-semver": "^3.0", - "google/proto-client-php": "^0.7", - "google/gax": "^0.6" + "google/proto-client-php": "^0.9", + "google/gax": "^0.8" }, "suggest": { "google/gax": "Required to support gRPC", diff --git a/src/ErrorReporting/V1beta1/ErrorGroupServiceClient.php b/src/ErrorReporting/V1beta1/ErrorGroupServiceClient.php index b979d00852d4..9522bd092f6d 100644 --- a/src/ErrorReporting/V1beta1/ErrorGroupServiceClient.php +++ b/src/ErrorReporting/V1beta1/ErrorGroupServiceClient.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/ErrorReporting/V1beta1/ErrorStatsServiceClient.php b/src/ErrorReporting/V1beta1/ErrorStatsServiceClient.php index 76dace7aaff3..188982f4e970 100644 --- a/src/ErrorReporting/V1beta1/ErrorStatsServiceClient.php +++ b/src/ErrorReporting/V1beta1/ErrorStatsServiceClient.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/ErrorReporting/V1beta1/ReportErrorsServiceClient.php b/src/ErrorReporting/V1beta1/ReportErrorsServiceClient.php index da495d97d945..c8255296fb4d 100644 --- a/src/ErrorReporting/V1beta1/ReportErrorsServiceClient.php +++ b/src/ErrorReporting/V1beta1/ReportErrorsServiceClient.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/ErrorReporting/V1beta1/resources/error_group_service_client_config.json b/src/ErrorReporting/V1beta1/resources/error_group_service_client_config.json index 422c0afc4c9d..4b57386b15c9 100644 --- a/src/ErrorReporting/V1beta1/resources/error_group_service_client_config.json +++ b/src/ErrorReporting/V1beta1/resources/error_group_service_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.devtools.clouderrorreporting.v1beta1.ErrorGroupService": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/ErrorReporting/V1beta1/resources/error_stats_service_client_config.json b/src/ErrorReporting/V1beta1/resources/error_stats_service_client_config.json index a1449cd540de..11e42f42ca8f 100644 --- a/src/ErrorReporting/V1beta1/resources/error_stats_service_client_config.json +++ b/src/ErrorReporting/V1beta1/resources/error_stats_service_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.devtools.clouderrorreporting.v1beta1.ErrorStatsService": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/ErrorReporting/V1beta1/resources/report_errors_service_client_config.json b/src/ErrorReporting/V1beta1/resources/report_errors_service_client_config.json index 42b3b2a8a9dc..48b02b3d6bad 100644 --- a/src/ErrorReporting/V1beta1/resources/report_errors_service_client_config.json +++ b/src/ErrorReporting/V1beta1/resources/report_errors_service_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.devtools.clouderrorreporting.v1beta1.ReportErrorsService": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/Logging/V2/ConfigServiceV2Client.php b/src/Logging/V2/ConfigServiceV2Client.php index 0138ef82dd34..ce0166f2f58d 100644 --- a/src/Logging/V2/ConfigServiceV2Client.php +++ b/src/Logging/V2/ConfigServiceV2Client.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/Logging/V2/LoggingServiceV2Client.php b/src/Logging/V2/LoggingServiceV2Client.php index 87e667ec9112..009fe849853a 100644 --- a/src/Logging/V2/LoggingServiceV2Client.php +++ b/src/Logging/V2/LoggingServiceV2Client.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/Logging/V2/MetricsServiceV2Client.php b/src/Logging/V2/MetricsServiceV2Client.php index 546a0e971cdc..2b4746ed976b 100644 --- a/src/Logging/V2/MetricsServiceV2Client.php +++ b/src/Logging/V2/MetricsServiceV2Client.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/Logging/V2/resources/config_service_v2_client_config.json b/src/Logging/V2/resources/config_service_v2_client_config.json index bdfd9c9e1626..10e0b3d342db 100644 --- a/src/Logging/V2/resources/config_service_v2_client_config.json +++ b/src/Logging/V2/resources/config_service_v2_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.logging.v2.ConfigServiceV2": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/Logging/V2/resources/logging_service_v2_client_config.json b/src/Logging/V2/resources/logging_service_v2_client_config.json index b7576ee7dadd..b8a2951b3336 100644 --- a/src/Logging/V2/resources/logging_service_v2_client_config.json +++ b/src/Logging/V2/resources/logging_service_v2_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.logging.v2.LoggingServiceV2": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { @@ -39,7 +39,12 @@ "WriteLogEntries": { "timeout_millis": 30000, "retry_codes_name": "non_idempotent", - "retry_params_name": "default" + "retry_params_name": "default", + "bundling": { + "element_count_threshold": 100, + "request_byte_threshold": 1024, + "delay_threshold_millis": 10 + } }, "ListLogEntries": { "timeout_millis": 30000, diff --git a/src/Logging/V2/resources/metrics_service_v2_client_config.json b/src/Logging/V2/resources/metrics_service_v2_client_config.json index 0400cb5bb638..4e6890a011d4 100644 --- a/src/Logging/V2/resources/metrics_service_v2_client_config.json +++ b/src/Logging/V2/resources/metrics_service_v2_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.logging.v2.MetricsServiceV2": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/Monitoring/V3/GroupServiceClient.php b/src/Monitoring/V3/GroupServiceClient.php index 709e537424ea..27e4f5348945 100644 --- a/src/Monitoring/V3/GroupServiceClient.php +++ b/src/Monitoring/V3/GroupServiceClient.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/Monitoring/V3/MetricServiceClient.php b/src/Monitoring/V3/MetricServiceClient.php index 8508874d5082..0b3924722397 100644 --- a/src/Monitoring/V3/MetricServiceClient.php +++ b/src/Monitoring/V3/MetricServiceClient.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/Monitoring/V3/resources/group_service_client_config.json b/src/Monitoring/V3/resources/group_service_client_config.json index c1d5d808d100..825193b3a411 100644 --- a/src/Monitoring/V3/resources/group_service_client_config.json +++ b/src/Monitoring/V3/resources/group_service_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.monitoring.v3.GroupService": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/Monitoring/V3/resources/metric_service_client_config.json b/src/Monitoring/V3/resources/metric_service_client_config.json index 4ec14c73bde7..b04f72277007 100644 --- a/src/Monitoring/V3/resources/metric_service_client_config.json +++ b/src/Monitoring/V3/resources/metric_service_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.monitoring.v3.MetricService": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/PubSub/V1/PublisherClient.php b/src/PubSub/V1/PublisherClient.php index 80131bea8db8..101156523195 100644 --- a/src/PubSub/V1/PublisherClient.php +++ b/src/PubSub/V1/PublisherClient.php @@ -1,6 +1,6 @@ null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; diff --git a/src/PubSub/V1/SubscriberClient.php b/src/PubSub/V1/SubscriberClient.php index abdab57f9b8a..52d055abbc34 100644 --- a/src/PubSub/V1/SubscriberClient.php +++ b/src/PubSub/V1/SubscriberClient.php @@ -1,6 +1,6 @@ render([ + 'project' => $project, + 'snapshot' => $snapshot, + ]); + } + /** * Formats a string containing the fully-qualified path to represent * a subscription resource. @@ -160,6 +182,24 @@ public static function parseProjectFromProjectName($projectName) return self::getProjectNameTemplate()->match($projectName)['project']; } + /** + * Parses the project from the given fully-qualified path which + * represents a snapshot resource. + */ + public static function parseProjectFromSnapshotName($snapshotName) + { + return self::getSnapshotNameTemplate()->match($snapshotName)['project']; + } + + /** + * Parses the snapshot from the given fully-qualified path which + * represents a snapshot resource. + */ + public static function parseSnapshotFromSnapshotName($snapshotName) + { + return self::getSnapshotNameTemplate()->match($snapshotName)['snapshot']; + } + /** * Parses the project from the given fully-qualified path which * represents a subscription resource. @@ -205,6 +245,15 @@ private static function getProjectNameTemplate() return self::$projectNameTemplate; } + private static function getSnapshotNameTemplate() + { + if (self::$snapshotNameTemplate == null) { + self::$snapshotNameTemplate = new PathTemplate('projects/{project}/snapshots/{snapshot}'); + } + + return self::$snapshotNameTemplate; + } + private static function getSubscriptionNameTemplate() { if (self::$subscriptionNameTemplate == null) { @@ -232,14 +281,43 @@ private static function getPageStreamingDescriptors() 'responsePageTokenField' => 'next_page_token', 'resourceField' => 'subscriptions', ]); + $listSnapshotsPageStreamingDescriptor = + new PageStreamingDescriptor([ + 'requestPageTokenField' => 'page_token', + 'requestPageSizeField' => 'page_size', + 'responsePageTokenField' => 'next_page_token', + 'resourceField' => 'snapshots', + ]); $pageStreamingDescriptors = [ 'listSubscriptions' => $listSubscriptionsPageStreamingDescriptor, + 'listSnapshots' => $listSnapshotsPageStreamingDescriptor, ]; return $pageStreamingDescriptors; } + private static function getGrpcStreamingDescriptors() + { + return [ + 'streamingPull' => [ + 'grpcStreamingType' => 'BidiStreaming', + 'resourcesField' => 'getReceivedMessagesList', + ], + ]; + } + + private static function getGapicVersion() + { + if (file_exists(__DIR__.'/../VERSION')) { + return trim(file_get_contents(__DIR__.'/../VERSION')); + } elseif (class_exists('\Google\Cloud\ServiceBuilder')) { + return \Google\Cloud\ServiceBuilder::VERSION; + } else { + return; + } + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -265,9 +343,6 @@ private static function getPageStreamingDescriptors() * that don't use retries. For calls that use retries, * set the timeout in RetryOptions. * Default: 30000 (30 seconds) - * @type string $appName The codename of the calling service. Default 'gax'. - * @type string $appVersion The version of the calling service. - * Default: the current version of GAX. * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. @@ -284,30 +359,35 @@ public function __construct($options = []) ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); + $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; $this->descriptors = [ 'createSubscription' => $defaultDescriptors, 'getSubscription' => $defaultDescriptors, + 'updateSubscription' => $defaultDescriptors, 'listSubscriptions' => $defaultDescriptors, 'deleteSubscription' => $defaultDescriptors, 'modifyAckDeadline' => $defaultDescriptors, 'acknowledge' => $defaultDescriptors, 'pull' => $defaultDescriptors, + 'streamingPull' => $defaultDescriptors, 'modifyPushConfig' => $defaultDescriptors, + 'listSnapshots' => $defaultDescriptors, + 'createSnapshot' => $defaultDescriptors, + 'deleteSnapshot' => $defaultDescriptors, + 'seek' => $defaultDescriptors, 'setIamPolicy' => $defaultDescriptors, 'getIamPolicy' => $defaultDescriptors, 'testIamPermissions' => $defaultDescriptors, @@ -316,6 +396,10 @@ public function __construct($options = []) foreach ($pageStreamingDescriptors as $method => $pageStreamingDescriptor) { $this->descriptors[$method]['pageStreamingDescriptor'] = $pageStreamingDescriptor; } + $grpcStreamingDescriptors = self::getGrpcStreamingDescriptors(); + foreach ($grpcStreamingDescriptors as $method => $grpcStreamingDescriptor) { + $this->descriptors[$method]['grpcStreamingDescriptor'] = $grpcStreamingDescriptor; + } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/subscriber_client_config.json'); $clientConfig = json_decode($clientConfigJsonString, true); @@ -424,6 +508,18 @@ public function __construct($options = []) * * If the subscriber never acknowledges the message, the Pub/Sub * system will eventually redeliver the message. + * @type bool $retainAckedMessages + * Indicates whether to retain acknowledged messages. If true, then + * messages are not expunged from the subscription's backlog, even if they are + * acknowledged, until they fall out of the `message_retention_duration` + * window. + * @type Duration $messageRetentionDuration + * How long to retain unacknowledged messages in the subscription's backlog, + * from the moment a message is published. + * If `retain_acked_messages` is true, then this also configures the retention + * of acknowledged messages, and thus configures how far back in time a `Seek` + * can be done. Defaults to 7 days. Cannot be more than 7 days or less than 10 + * minutes. * @type \Google\GAX\RetrySettings $retrySettings * Retry settings to use for this call. If present, then * $timeoutMillis is ignored. @@ -447,6 +543,12 @@ public function createSubscription($name, $topic, $optionalArgs = []) if (isset($optionalArgs['ackDeadlineSeconds'])) { $request->setAckDeadlineSeconds($optionalArgs['ackDeadlineSeconds']); } + if (isset($optionalArgs['retainAckedMessages'])) { + $request->setRetainAckedMessages($optionalArgs['retainAckedMessages']); + } + if (isset($optionalArgs['messageRetentionDuration'])) { + $request->setMessageRetentionDuration($optionalArgs['messageRetentionDuration']); + } $mergedSettings = $this->defaultCallSettings['createSubscription']->merge( new CallSettings($optionalArgs) @@ -516,6 +618,62 @@ public function getSubscription($subscription, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * Updates an existing subscription. Note that certain properties of a + * subscription, such as its topic, are not modifiable. + * + * Sample code: + * ``` + * try { + * $subscriberClient = new SubscriberClient(); + * $subscription = new Subscription(); + * $updateMask = new FieldMask(); + * $response = $subscriberClient->updateSubscription($subscription, $updateMask); + * } finally { + * $subscriberClient->close(); + * } + * ``` + * + * @param Subscription $subscription The updated subscription object. + * @param FieldMask $updateMask Indicates which fields in the provided subscription to update. + * Must be specified and non-empty. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\pubsub\v1\Subscription + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function updateSubscription($subscription, $updateMask, $optionalArgs = []) + { + $request = new UpdateSubscriptionRequest(); + $request->setSubscription($subscription); + $request->setUpdateMask($updateMask); + + $mergedSettings = $this->defaultCallSettings['updateSubscription']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->subscriberStub, + 'UpdateSubscription', + $mergedSettings, + $this->descriptors['updateSubscription'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Lists matching subscriptions. * @@ -848,6 +1006,89 @@ public function pull($subscription, $maxMessages, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * (EXPERIMENTAL) StreamingPull is an experimental feature. This RPC will + * respond with UNIMPLEMENTED errors unless you have been invited to test + * this feature. Contact cloud-pubsub@google.com with any questions. + * + * Establishes a stream with the server, which sends messages down to the + * client. The client streams acknowledgements and ack deadline modifications + * back to the server. The server will close the stream and return the status + * on any error. The server may close the stream with status `OK` to reassign + * server-side resources, in which case, the client should re-establish the + * stream. `UNAVAILABLE` may also be returned in the case of a transient error + * (e.g., a server restart). These should also be retried by the client. Flow + * control can be achieved by configuring the underlying RPC channel. + * + * Sample code: + * ``` + * try { + * $subscriberClient = new SubscriberClient(); + * $formattedSubscription = SubscriberClient::formatSubscriptionName("[PROJECT]", "[SUBSCRIPTION]"); + * $streamAckDeadlineSeconds = 0; + * $request = new StreamingPullRequest(); + * $request->setSubscription($formattedSubscription); + * $request->setStreamAckDeadlineSeconds($streamAckDeadlineSeconds); + * $requests = [$request]; + * + * // Write all requests to the server, then read all responses until the + * // stream is complete + * $stream = $subscriberClient->streamingPull(); + * $stream->writeAll($requests); + * foreach ($stream->closeWriteAndReadAll() as $element) { + * // doSomethingWith($element); + * } + * + * // OR write requests individually, making read() calls if + * // required. Call closeWrite() once writes are complete, and read the + * // remaining responses from the server. + * $stream = $subscriberClient->streamingPull(); + * foreach ($requests as $request) { + * $stream->write($request); + * // if required, read a single response from the stream + * $element = $stream->read(); + * // doSomethingWith($element) + * } + * $stream->closeWrite(); + * $element = $stream->read(); + * while (!is_null($element)) { + * // doSomethingWith($element) + * $element = $stream->read(); + * } + * } finally { + * $subscriberClient->close(); + * } + * ``` + * + * @param array $optionalArgs { + * Optional. + * + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * + * @return \Google\GAX\BidiStreamingResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function streamingPull($optionalArgs = []) + { + $mergedSettings = $this->defaultCallSettings['streamingPull']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->subscriberStub, + 'StreamingPull', + $mergedSettings, + $this->descriptors['streamingPull'] + ); + + return $callable( + null, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Modifies the `PushConfig` for a specified subscription. * @@ -911,6 +1152,287 @@ public function modifyPushConfig($subscription, $pushConfig, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * Lists the existing snapshots. + * + * Sample code: + * ``` + * try { + * $subscriberClient = new SubscriberClient(); + * $formattedProject = SubscriberClient::formatProjectName("[PROJECT]"); + * // Iterate through all elements + * $pagedResponse = $subscriberClient->listSnapshots($formattedProject); + * foreach ($pagedResponse->iterateAllElements() as $element) { + * // doSomethingWith($element); + * } + * + * // OR iterate over pages of elements, with the maximum page size set to 5 + * $pagedResponse = $subscriberClient->listSnapshots($formattedProject, ['pageSize' => 5]); + * foreach ($pagedResponse->iteratePages() as $page) { + * foreach ($page as $element) { + * // doSomethingWith($element); + * } + * } + * } finally { + * $subscriberClient->close(); + * } + * ``` + * + * @param string $project The name of the cloud project that snapshots belong to. + * Format is `projects/{project}`. + * @param array $optionalArgs { + * Optional. + * + * @type int $pageSize + * The maximum number of resources contained in the underlying API + * response. The API may return fewer values in a page, even if + * there are additional values to be retrieved. + * @type string $pageToken + * A page token is used to specify a page of values to be returned. + * If no page token is specified (the default), the first page + * of values will be returned. Any page token used here must have + * been generated by a previous call to the API. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \Google\GAX\PagedListResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function listSnapshots($project, $optionalArgs = []) + { + $request = new ListSnapshotsRequest(); + $request->setProject($project); + if (isset($optionalArgs['pageSize'])) { + $request->setPageSize($optionalArgs['pageSize']); + } + if (isset($optionalArgs['pageToken'])) { + $request->setPageToken($optionalArgs['pageToken']); + } + + $mergedSettings = $this->defaultCallSettings['listSnapshots']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->subscriberStub, + 'ListSnapshots', + $mergedSettings, + $this->descriptors['listSnapshots'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Creates a snapshot from the requested subscription. + * If the snapshot already exists, returns `ALREADY_EXISTS`. + * If the requested subscription doesn't exist, returns `NOT_FOUND`. + * + * If the name is not provided in the request, the server will assign a random + * name for this snapshot on the same project as the subscription, conforming + * to the + * [resource name format](https://cloud.google.com/pubsub/docs/overview#names). + * The generated name is populated in the returned Snapshot object. + * Note that for REST API requests, you must specify a name in the request. + * + * Sample code: + * ``` + * try { + * $subscriberClient = new SubscriberClient(); + * $formattedName = SubscriberClient::formatSnapshotName("[PROJECT]", "[SNAPSHOT]"); + * $formattedSubscription = SubscriberClient::formatSubscriptionName("[PROJECT]", "[SUBSCRIPTION]"); + * $response = $subscriberClient->createSnapshot($formattedName, $formattedSubscription); + * } finally { + * $subscriberClient->close(); + * } + * ``` + * + * @param string $name Optional user-provided name for this snapshot. + * If the name is not provided in the request, the server will assign a random + * name for this snapshot on the same project as the subscription. + * Note that for REST API requests, you must specify a name. + * Format is `projects/{project}/snapshots/{snap}`. + * @param string $subscription The subscription whose backlog the snapshot retains. + * Specifically, the created snapshot is guaranteed to retain: + * (a) The existing backlog on the subscription. More precisely, this is + * defined as the messages in the subscription's backlog that are + * unacknowledged upon the successful completion of the + * `CreateSnapshot` request; as well as: + * (b) Any messages published to the subscription's topic following the + * successful completion of the CreateSnapshot request. + * Format is `projects/{project}/subscriptions/{sub}`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\pubsub\v1\Snapshot + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function createSnapshot($name, $subscription, $optionalArgs = []) + { + $request = new CreateSnapshotRequest(); + $request->setName($name); + $request->setSubscription($subscription); + + $mergedSettings = $this->defaultCallSettings['createSnapshot']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->subscriberStub, + 'CreateSnapshot', + $mergedSettings, + $this->descriptors['createSnapshot'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Removes an existing snapshot. All messages retained in the snapshot + * are immediately dropped. After a snapshot is deleted, a new one may be + * created with the same name, but the new one has no association with the old + * snapshot or its subscription, unless the same subscription is specified. + * + * Sample code: + * ``` + * try { + * $subscriberClient = new SubscriberClient(); + * $formattedSnapshot = SubscriberClient::formatSnapshotName("[PROJECT]", "[SNAPSHOT]"); + * $subscriberClient->deleteSnapshot($formattedSnapshot); + * } finally { + * $subscriberClient->close(); + * } + * ``` + * + * @param string $snapshot The name of the snapshot to delete. + * Format is `projects/{project}/snapshots/{snap}`. + * @param array $optionalArgs { + * Optional. + * + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function deleteSnapshot($snapshot, $optionalArgs = []) + { + $request = new DeleteSnapshotRequest(); + $request->setSnapshot($snapshot); + + $mergedSettings = $this->defaultCallSettings['deleteSnapshot']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->subscriberStub, + 'DeleteSnapshot', + $mergedSettings, + $this->descriptors['deleteSnapshot'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + + /** + * Seeks an existing subscription to a point in time or to a given snapshot, + * whichever is provided in the request. + * + * Sample code: + * ``` + * try { + * $subscriberClient = new SubscriberClient(); + * $formattedSubscription = SubscriberClient::formatSubscriptionName("[PROJECT]", "[SUBSCRIPTION]"); + * $response = $subscriberClient->seek($formattedSubscription); + * } finally { + * $subscriberClient->close(); + * } + * ``` + * + * @param string $subscription The subscription to affect. + * @param array $optionalArgs { + * Optional. + * + * @type Timestamp $time + * The time to seek to. + * Messages retained in the subscription that were published before this + * time are marked as acknowledged, and messages retained in the + * subscription that were published after this time are marked as + * unacknowledged. Note that this operation affects only those messages + * retained in the subscription (configured by the combination of + * `message_retention_duration` and `retain_acked_messages`). For example, + * if `time` corresponds to a point before the message retention + * window (or to a point before the system's notion of the subscription + * creation time), only retained messages will be marked as unacknowledged, + * and already-expunged messages will not be restored. + * @type string $snapshot + * The snapshot to seek to. The snapshot's topic must be the same as that of + * the provided subscription. + * Format is `projects/{project}/snapshots/{snap}`. + * @type \Google\GAX\RetrySettings $retrySettings + * Retry settings to use for this call. If present, then + * $timeoutMillis is ignored. + * @type int $timeoutMillis + * Timeout to use for this call. Only used if $retrySettings + * is not set. + * } + * + * @return \google\pubsub\v1\SeekResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function seek($subscription, $optionalArgs = []) + { + $request = new SeekRequest(); + $request->setSubscription($subscription); + if (isset($optionalArgs['time'])) { + $request->setTime($optionalArgs['time']); + } + if (isset($optionalArgs['snapshot'])) { + $request->setSnapshot($optionalArgs['snapshot']); + } + + $mergedSettings = $this->defaultCallSettings['seek']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->subscriberStub, + 'Seek', + $mergedSettings, + $this->descriptors['seek'] + ); + + return $callable( + $request, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Sets the access control policy on the specified resource. Replaces any * existing policy. diff --git a/src/PubSub/V1/resources/publisher_client_config.json b/src/PubSub/V1/resources/publisher_client_config.json index 4df74e5170e0..99ebf2412f29 100644 --- a/src/PubSub/V1/resources/publisher_client_config.json +++ b/src/PubSub/V1/resources/publisher_client_config.json @@ -2,17 +2,17 @@ "interfaces": { "google.pubsub.v1.Publisher": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "one_plus_delivery": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "one_plus_delivery": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { diff --git a/src/PubSub/V1/resources/subscriber_client_config.json b/src/PubSub/V1/resources/subscriber_client_config.json index c39ebcb51e63..8fbb5fb13631 100644 --- a/src/PubSub/V1/resources/subscriber_client_config.json +++ b/src/PubSub/V1/resources/subscriber_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.pubsub.v1.Subscriber": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { @@ -41,6 +41,11 @@ "retry_codes_name": "idempotent", "retry_params_name": "default" }, + "UpdateSubscription": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, "ListSubscriptions": { "timeout_millis": 60000, "retry_codes_name": "idempotent", @@ -66,11 +71,36 @@ "retry_codes_name": "non_idempotent", "retry_params_name": "messaging" }, + "StreamingPull": { + "timeout_millis": 60000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "messaging" + }, "ModifyPushConfig": { "timeout_millis": 60000, "retry_codes_name": "non_idempotent", "retry_params_name": "default" }, + "ListSnapshots": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "CreateSnapshot": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "DeleteSnapshot": { + "timeout_millis": 60000, + "retry_codes_name": "idempotent", + "retry_params_name": "default" + }, + "Seek": { + "timeout_millis": 60000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" + }, "SetIamPolicy": { "timeout_millis": 60000, "retry_codes_name": "non_idempotent", diff --git a/src/Speech/V1beta1/SpeechClient.php b/src/Speech/V1beta1/SpeechClient.php index 3634a7eb46a9..2e2e7997c0d1 100644 --- a/src/Speech/V1beta1/SpeechClient.php +++ b/src/Speech/V1beta1/SpeechClient.php @@ -1,6 +1,6 @@ setEncoding($encoding); + * $config->setSampleRate($sampleRate); + * $uri = "gs://bucket_name/file_name.flac"; * $audio = new RecognitionAudio(); + * $audio->setUri($uri); * $response = $speechClient->syncRecognize($config, $audio); * } finally { * $speechClient->close(); @@ -92,7 +100,7 @@ class SpeechClient /** * The code generator version, to be included in the agent header. */ - const CODEGEN_VERSION = '0.1.0'; + const CODEGEN_VERSION = '0.0.5'; private $grpcCredentialsHelper; private $speechStub; @@ -111,6 +119,26 @@ private static function getLongRunningDescriptors() ]; } + private static function getGrpcStreamingDescriptors() + { + return [ + 'streamingRecognize' => [ + 'grpcStreamingType' => 'BidiStreaming', + ], + ]; + } + + private static function getGapicVersion() + { + if (file_exists(__DIR__.'/../VERSION')) { + return trim(file_get_contents(__DIR__.'/../VERSION')); + } elseif (class_exists('\Google\Cloud\ServiceBuilder')) { + return \Google\Cloud\ServiceBuilder::VERSION; + } else { + return; + } + } + /** * Return an OperationsClient object with the same endpoint as $this. * @@ -121,6 +149,32 @@ public function getOperationsClient() return $this->operationsClient; } + /** + * Resume an existing long running operation that was previously started + * by a long running API method. If $methodName is not provided, or does + * not match a long running API method, then the operation can still be + * resumed, but the OperationResponse object will not deserialize the + * final response. + * + * @param string $operationName The name of the long running operation + * @param string $methodName The name of the method used to start the operation + * + * @return \Google\GAX\OperationResponse + */ + public function resumeOperation($operationName, $methodName = null) + { + $lroDescriptors = self::getLongRunningDescriptors(); + if (!is_null($methodName) && array_key_exists($methodName, $lroDescriptors)) { + $options = $lroDescriptors[$methodName]; + } else { + $options = []; + } + $operation = new OperationResponse($operationName, $this->getOperationsClient(), $options); + $operation->reload(); + + return $operation; + } + // TODO(garrettjones): add channel (when supported in gRPC) /** * Constructor. @@ -146,9 +200,6 @@ public function getOperationsClient() * that don't use retries. For calls that use retries, * set the timeout in RetryOptions. * Default: 30000 (30 seconds) - * @type string $appName The codename of the calling service. Default 'gax'. - * @type string $appVersion The version of the calling service. - * Default: the current version of GAX. * @type \Google\Auth\CredentialsLoader $credentialsLoader * A CredentialsLoader object created using the * Google\Auth library. @@ -164,34 +215,44 @@ public function __construct($options = []) ], 'retryingOverride' => null, 'timeoutMillis' => self::DEFAULT_TIMEOUT_MILLIS, - 'appName' => 'gax', - 'appVersion' => AgentHeaderDescriptor::getGaxVersion(), + 'libName' => null, + 'libVersion' => null, ]; $options = array_merge($defaultOptions, $options); - $this->operationsClient = new OperationsClient([ - 'serviceAddress' => $options['serviceAddress'], - 'scopes' => $options['scopes'], - ]); + if (array_key_exists('operationsClient', $options)) { + $this->operationsClient = $options['operationsClient']; + } else { + $this->operationsClient = new OperationsClient([ + 'serviceAddress' => $options['serviceAddress'], + 'scopes' => $options['scopes'], + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + ]); + } + + $gapicVersion = $options['libVersion'] ?: self::getGapicVersion(); $headerDescriptor = new AgentHeaderDescriptor([ - 'clientName' => $options['appName'], - 'clientVersion' => $options['appVersion'], - 'codeGenName' => self::CODEGEN_NAME, - 'codeGenVersion' => self::CODEGEN_VERSION, - 'gaxVersion' => AgentHeaderDescriptor::getGaxVersion(), - 'phpVersion' => phpversion(), + 'libName' => $options['libName'], + 'libVersion' => $options['libVersion'], + 'gapicVersion' => $gapicVersion, ]); $defaultDescriptors = ['headerDescriptor' => $headerDescriptor]; $this->descriptors = [ 'syncRecognize' => $defaultDescriptors, 'asyncRecognize' => $defaultDescriptors, + 'streamingRecognize' => $defaultDescriptors, ]; $longRunningDescriptors = self::getLongRunningDescriptors(); foreach ($longRunningDescriptors as $method => $longRunningDescriptor) { $this->descriptors[$method]['longRunningDescriptor'] = $longRunningDescriptor + ['operationsClient' => $this->operationsClient]; } + $grpcStreamingDescriptors = self::getGrpcStreamingDescriptors(); + foreach ($grpcStreamingDescriptors as $method => $grpcStreamingDescriptor) { + $this->descriptors[$method]['grpcStreamingDescriptor'] = $grpcStreamingDescriptor; + } $clientConfigJsonString = file_get_contents(__DIR__.'/resources/speech_client_config.json'); $clientConfig = json_decode($clientConfigJsonString, true); @@ -235,8 +296,14 @@ public function __construct($options = []) * ``` * try { * $speechClient = new SpeechClient(); + * $encoding = AudioEncoding::FLAC; + * $sampleRate = 44100; * $config = new RecognitionConfig(); + * $config->setEncoding($encoding); + * $config->setSampleRate($sampleRate); + * $uri = "gs://bucket_name/file_name.flac"; * $audio = new RecognitionAudio(); + * $audio->setUri($uri); * $response = $speechClient->syncRecognize($config, $audio); * } finally { * $speechClient->close(); @@ -293,8 +360,14 @@ public function syncRecognize($config, $audio, $optionalArgs = []) * ``` * try { * $speechClient = new SpeechClient(); + * $encoding = AudioEncoding::FLAC; + * $sampleRate = 44100; * $config = new RecognitionConfig(); + * $config->setEncoding($encoding); + * $config->setSampleRate($sampleRate); + * $uri = "gs://bucket_name/file_name.flac"; * $audio = new RecognitionAudio(); + * $audio->setUri($uri); * $operationResponse = $speechClient->asyncRecognize($config, $audio); * $operationResponse->pollUntilComplete(); * if ($operationResponse->operationSucceeded()) { @@ -304,6 +377,23 @@ public function syncRecognize($config, $audio, $optionalArgs = []) * $error = $operationResponse->getError(); * // handleError($error) * } + * + * // OR start the operation, keep the operation name, and resume later + * $operationResponse = $speechClient->asyncRecognize($config, $audio); + * $operationName = $operationResponse->getName(); + * // ... do other work + * $newOperationResponse = $speechClient->resumeOperation($operationName, 'asyncRecognize'); + * while (!$newOperationResponse->isDone()) { + * // ... do other work + * $newOperationResponse->reload(); + * } + * if ($newOperationResponse->operationSucceeded()) { + * $result = $newOperationResponse->getResult(); + * // doSomethingWith($result) + * } else { + * $error = $newOperationResponse->getError(); + * // handleError($error) + * } * } finally { * $speechClient->close(); * } @@ -349,6 +439,75 @@ public function asyncRecognize($config, $audio, $optionalArgs = []) ['call_credentials_callback' => $this->createCredentialsCallback()]); } + /** + * Perform bidirectional streaming speech-recognition: receive results while + * sending audio. This method is only available via the gRPC API (not REST). + * + * Sample code: + * ``` + * try { + * $speechClient = new SpeechClient(); + * $request = new StreamingRecognizeRequest(); + * $requests = [$request]; + * + * // Write all requests to the server, then read all responses until the + * // stream is complete + * $stream = $speechClient->streamingRecognize(); + * $stream->writeAll($requests); + * foreach ($stream->closeWriteAndReadAll() as $element) { + * // doSomethingWith($element); + * } + * + * // OR write requests individually, making read() calls if + * // required. Call closeWrite() once writes are complete, and read the + * // remaining responses from the server. + * $stream = $speechClient->streamingRecognize(); + * foreach ($requests as $request) { + * $stream->write($request); + * // if required, read a single response from the stream + * $element = $stream->read(); + * // doSomethingWith($element) + * } + * $stream->closeWrite(); + * $element = $stream->read(); + * while (!is_null($element)) { + * // doSomethingWith($element) + * $element = $stream->read(); + * } + * } finally { + * $speechClient->close(); + * } + * ``` + * + * @param array $optionalArgs { + * Optional. + * + * @type int $timeoutMillis + * Timeout to use for this call. + * } + * + * @return \Google\GAX\BidiStreamingResponse + * + * @throws \Google\GAX\ApiException if the remote call fails + */ + public function streamingRecognize($optionalArgs = []) + { + $mergedSettings = $this->defaultCallSettings['streamingRecognize']->merge( + new CallSettings($optionalArgs) + ); + $callable = ApiCallable::createApiCall( + $this->speechStub, + 'StreamingRecognize', + $mergedSettings, + $this->descriptors['streamingRecognize'] + ); + + return $callable( + null, + [], + ['call_credentials_callback' => $this->createCredentialsCallback()]); + } + /** * Initiates an orderly shutdown in which preexisting calls continue but new * calls are immediately cancelled. diff --git a/src/Speech/V1beta1/resources/speech_client_config.json b/src/Speech/V1beta1/resources/speech_client_config.json index bbd5b8b62499..5d11ce19e587 100644 --- a/src/Speech/V1beta1/resources/speech_client_config.json +++ b/src/Speech/V1beta1/resources/speech_client_config.json @@ -2,13 +2,13 @@ "interfaces": { "google.cloud.speech.v1beta1.Speech": { "retry_codes": { - "retry_codes_def": { - "idempotent": [ - "DEADLINE_EXCEEDED", - "UNAVAILABLE" - ], - "non_idempotent": [] - } + "idempotent": [ + "DEADLINE_EXCEEDED", + "UNAVAILABLE" + ], + "non_idempotent": [ + "UNAVAILABLE" + ] }, "retry_params": { "default": { @@ -31,6 +31,11 @@ "timeout_millis": 60000, "retry_codes_name": "idempotent", "retry_params_name": "default" + }, + "StreamingRecognize": { + "timeout_millis": 190000, + "retry_codes_name": "non_idempotent", + "retry_params_name": "default" } } } From 0c15f7e403308230320969bca9e41af2d3bce1ac Mon Sep 17 00:00:00 2001 From: michaelbausor Date: Thu, 2 Mar 2017 12:02:01 -0800 Subject: [PATCH 077/107] Update headers (#366) * Update headers to new format * Update RequestWrapperTest --- src/GrpcTrait.php | 4 ++-- src/RequestWrapper.php | 1 + tests/unit/GrpcTraitTest.php | 4 ++-- tests/unit/RequestWrapperTest.php | 4 +++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/GrpcTrait.php b/src/GrpcTrait.php index ffc0fcd6136e..6aecb4314380 100644 --- a/src/GrpcTrait.php +++ b/src/GrpcTrait.php @@ -75,8 +75,8 @@ private function getGaxConfig() return [ 'credentialsLoader' => $this->requestWrapper->getCredentialsFetcher(), 'enableCaching' => false, - 'appName' => 'gcloud-php', - 'appVersion' => ServiceBuilder::VERSION + 'libName' => 'gccl', + 'libVersion' => ServiceBuilder::VERSION ]; } diff --git a/src/RequestWrapper.php b/src/RequestWrapper.php index 66d3e6579aae..2fe03d48b990 100644 --- a/src/RequestWrapper.php +++ b/src/RequestWrapper.php @@ -147,6 +147,7 @@ private function signRequest(RequestInterface $request) { $headers = [ 'User-Agent' => 'gcloud-php/' . ServiceBuilder::VERSION, + 'x-goog-api-client' => 'gl-php/' . phpversion() . ' gccl/' . ServiceBuilder::VERSION, 'Authorization' => 'Bearer ' . $this->getToken() ]; diff --git a/tests/unit/GrpcTraitTest.php b/tests/unit/GrpcTraitTest.php index 0be14e92e096..ae8e77af5330 100644 --- a/tests/unit/GrpcTraitTest.php +++ b/tests/unit/GrpcTraitTest.php @@ -71,8 +71,8 @@ public function testGetsGaxConfig() $expected = [ 'credentialsLoader' => $fetcher, 'enableCaching' => false, - 'appName' => 'gcloud-php', - 'appVersion' => ServiceBuilder::VERSION + 'libName' => 'gccl', + 'libVersion' => ServiceBuilder::VERSION ]; $this->assertEquals($expected, $this->implementation->call('getGaxConfig')); diff --git a/tests/unit/RequestWrapperTest.php b/tests/unit/RequestWrapperTest.php index e348b2361384..c18707b908ec 100644 --- a/tests/unit/RequestWrapperTest.php +++ b/tests/unit/RequestWrapperTest.php @@ -159,12 +159,14 @@ public function keyFileCredentialsProvider() ]; } - public function testAddsUserAgentToRequest() + public function testAddsUserAgentAndXGoogApiClientToRequest() { $requestWrapper = new RequestWrapper([ 'httpHandler' => function ($request, $options = []) { $userAgent = $request->getHeaderLine('User-Agent'); $this->assertEquals('gcloud-php/' . ServiceBuilder::VERSION, $userAgent); + $xGoogApiClient = $request->getHeaderLine('x-goog-api-client'); + $this->assertEquals('gl-php/' . phpversion() . ' gccl/' . ServiceBuilder::VERSION, $xGoogApiClient); return new Response(200); }, 'accessToken' => 'abc' From 1b5e7bc872ac28a2f5902ea5bd8725573b4cfc41 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 2 Mar 2017 22:00:34 -0500 Subject: [PATCH 078/107] export table as json instead of csv (#371) --- tests/system/BigQuery/ManageTablesTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/system/BigQuery/ManageTablesTest.php b/tests/system/BigQuery/ManageTablesTest.php index a2f3267fe052..7dc53933d9b9 100644 --- a/tests/system/BigQuery/ManageTablesTest.php +++ b/tests/system/BigQuery/ManageTablesTest.php @@ -97,7 +97,11 @@ public function testExportsTable() uniqid(self::TESTING_PREFIX) ); self::$deletionQueue[] = $object; - $job = self::$table->export($object); + $job = self::$table->export($object, [ + 'jobConfig' => [ + 'destinationFormat' => 'NEWLINE_DELIMITED_JSON' + ] + ]); $backoff = new ExponentialBackoff(8); $backoff->execute(function () use ($job) { @@ -111,6 +115,7 @@ public function testExportsTable() if (!$job->isComplete()) { $this->fail('Job failed to complete within the allotted time.'); } + $this->assertArrayNotHasKey('errorResult', $job->info()['status']); } From daa6ecb034cd176edf0f9abb88eb9c404c73e2a5 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Thu, 2 Mar 2017 22:01:25 -0500 Subject: [PATCH 079/107] ensure timestamps retain microsecond precision (#372) --- src/BigQuery/ValueMapper.php | 49 +++++++++++++++++++++++-- tests/unit/BigQuery/ValueMapperTest.php | 17 ++++++++- 2 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/BigQuery/ValueMapper.php b/src/BigQuery/ValueMapper.php index 2d6bfe0b4815..9121dcc2b584 100644 --- a/src/BigQuery/ValueMapper.php +++ b/src/BigQuery/ValueMapper.php @@ -106,10 +106,7 @@ public function fromBigQuery(array $value, array $schema) case self::TYPE_TIME: return new Time(new \DateTime($value)); case self::TYPE_TIMESTAMP: - $timestamp = new \DateTime(); - $timestamp->setTimestamp((float) $value); - - return new Timestamp($timestamp); + return $this->timestampFromBigQuery($value); case self::TYPE_RECORD: return $this->recordFromBigQuery($value, $schema['fields']); default: @@ -325,4 +322,48 @@ private function assocArrayToParameter(array $struct) ['structValues' => $values] ]; } + + /** + * Converts a timestamp in string format received from BigQuery to a + * {@see Google\Cloud\BigQuery\Timestamp}. + * + * @param string $value The timestamp. + * @return Timestamp + */ + private function timestampFromBigQuery($value) + { + // If the string contains 'E' convert from exponential notation to + // decimal notation. This doesn't cast to a float because precision can + // be lost. + if (strpos($value, 'E')) { + list($value, $exponent) = explode('E', $value); + list($firstDigit, $remainingDigits) = explode('.', $value); + + if (strlen($remainingDigits) > $exponent) { + $value = $firstDigit . substr_replace($remainingDigits, '.', $exponent, 0); + } else { + $value = $firstDigit . str_pad($remainingDigits, $exponent, '0') . '.0'; + } + } + + list($unixTimestamp, $microSeconds) = explode('.', $value); + $dateTime = new \DateTime("@$unixTimestamp"); + + // If the timestamp is before the epoch, make sure we account for that + // before concatenating the microseconds. + if ($microSeconds > 0 && $unixTimestamp[0] === '-') { + $microSeconds = 1000000 - (int) str_pad($microSeconds, 6, '0'); + $dateTime->modify('-1 second'); + } + + return new Timestamp( + new \DateTime( + sprintf( + '%s.%s+00:00', + $dateTime->format('Y-m-d H:i:s'), + $microSeconds + ) + ) + ); + } } diff --git a/tests/unit/BigQuery/ValueMapperTest.php b/tests/unit/BigQuery/ValueMapperTest.php index 58a57ccc2556..157ba271958a 100644 --- a/tests/unit/BigQuery/ValueMapperTest.php +++ b/tests/unit/BigQuery/ValueMapperTest.php @@ -134,10 +134,25 @@ public function bigQueryValueProvider() new Time(new \DateTime('12:15:15')) ], [ - ['v' => '1438712914'], + ['v' => '1.438712914E9'], ['type' => 'TIMESTAMP'], new Timestamp(new \DateTime('2015-08-04 18:28:34Z')) ], + [ + ['v' => '2678400.0'], + ['type' => 'TIMESTAMP'], + new Timestamp(new \DateTime('1970-02-01')) + ], + [ + ['v' => '-3.1561919984985E8'], + ['type' => 'TIMESTAMP'], + new Timestamp(new \DateTime('1960-01-01 00:00:00.150150Z')) + ], + [ + ['v' => '9.4668480015015E8'], + ['type' => 'TIMESTAMP'], + new Timestamp(new \DateTime('2000-01-01 00:00:00.150150Z')) + ], [ [ 'v' => [ From eb141eb5da2faed98d437bac6d4d02b801625079 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Thu, 2 Mar 2017 22:14:21 -0500 Subject: [PATCH 080/107] Prepare v0.22.0 (#370) --- docs/manifest.json | 1 + src/ServiceBuilder.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 5efcfca27413..718437aa6dd4 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -7,6 +7,7 @@ "matchPartialServiceId": true, "markdown": "php", "versions": [ + "v0.22.0", "v0.21.1", "v0.21.0", "v0.20.2", diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index 097653507b88..232ae57e5c6e 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -48,7 +48,7 @@ */ class ServiceBuilder { - const VERSION = '0.21.1'; + const VERSION = '0.22.0'; /** * @var array Configuration options to be used between clients. From 9b06e342c30d30736210d24b0b48d04a4d4ed162 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Fri, 3 Mar 2017 15:30:51 -0500 Subject: [PATCH 081/107] Add sleep after insertBatch operation (#379) --- tests/system/Datastore/RunQueryTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/system/Datastore/RunQueryTest.php b/tests/system/Datastore/RunQueryTest.php index 0677fe9e1339..32c34693a4ec 100644 --- a/tests/system/Datastore/RunQueryTest.php +++ b/tests/system/Datastore/RunQueryTest.php @@ -63,6 +63,12 @@ public static function setUpBeforeClass() self::$client->entity($key2, self::$data[2]), self::$client->entity($key3, self::$data[3]) ]); + + // on rare occasions the queries below are returning no results when + // triggered immediately after an insert operation. the sleep here + // is intended to help alleviate this issue. + sleep(1); + self::$deletionQueue[] = self::$ancestor; self::$deletionQueue[] = $key1; self::$deletionQueue[] = $key2; From 66f1789e8b4a3b78eadd0eb0cc66b2aae91662b9 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Fri, 3 Mar 2017 15:31:58 -0500 Subject: [PATCH 082/107] ensure user agents are sent when using an API key (#378) --- src/RequestWrapper.php | 15 ++++++++------- tests/unit/RequestWrapperTest.php | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/RequestWrapper.php b/src/RequestWrapper.php index 2fe03d48b990..7dbf528c5e31 100644 --- a/src/RequestWrapper.php +++ b/src/RequestWrapper.php @@ -128,29 +128,30 @@ public function send(RequestInterface $request, array $options = []) $httpOptions = isset($options['httpOptions']) ? $options['httpOptions'] : $this->httpOptions; $backoff = new ExponentialBackoff($retries, $this->getRetryFunction()); - $signedRequest = $this->shouldSignRequest ? $this->signRequest($request) : $request; - try { - return $backoff->execute($this->httpHandler, [$signedRequest, $httpOptions]); + return $backoff->execute($this->httpHandler, [$this->applyHeaders($request), $httpOptions]); } catch (\Exception $ex) { throw $this->convertToGoogleException($ex); } } /** - * Sign the request. + * Applies headers to the request. * * @param RequestInterface $request Psr7 request. * @return RequestInterface */ - private function signRequest(RequestInterface $request) + private function applyHeaders(RequestInterface $request) { $headers = [ 'User-Agent' => 'gcloud-php/' . ServiceBuilder::VERSION, - 'x-goog-api-client' => 'gl-php/' . phpversion() . ' gccl/' . ServiceBuilder::VERSION, - 'Authorization' => 'Bearer ' . $this->getToken() + 'x-goog-api-client' => 'gl-php/' . phpversion() . ' gccl/' . ServiceBuilder::VERSION ]; + if ($this->shouldSignRequest) { + $headers['Authorization'] = 'Bearer ' . $this->getToken(); + } + return Psr7\modify_request($request, ['set_headers' => $headers]); } diff --git a/tests/unit/RequestWrapperTest.php b/tests/unit/RequestWrapperTest.php index c18707b908ec..a1c453431b60 100644 --- a/tests/unit/RequestWrapperTest.php +++ b/tests/unit/RequestWrapperTest.php @@ -194,6 +194,26 @@ public function testAddsTokenToRequest() ); } + public function testRequestUsesApiKeyInsteadOfAuthHeader() + { + $requestWrapper = new RequestWrapper([ + 'httpHandler' => function ($request, $options = []) { + $authHeader = $request->getHeaderLine('Authorization'); + $userAgent = $request->getHeaderLine('User-Agent'); + $xGoogApiClient = $request->getHeaderLine('x-goog-api-client'); + $this->assertEquals('gcloud-php/' . ServiceBuilder::VERSION, $userAgent); + $this->assertEquals('gl-php/' . phpversion() . ' gccl/' . ServiceBuilder::VERSION, $xGoogApiClient); + $this->assertEmpty($authHeader); + return new Response(200); + }, + 'shouldSignRequest' => false + ]); + + $requestWrapper->send( + new Request('GET', 'http://www.example.com') + ); + } + /** * @expectedException Google\Cloud\Exception\GoogleException */ From 6c8c1f44f1cb07d98fbde20bd87047fbc54c97f1 Mon Sep 17 00:00:00 2001 From: michaelbausor Date: Fri, 3 Mar 2017 12:32:28 -0800 Subject: [PATCH 083/107] Update gapic docs (#377) --- src/Logging/V2/ConfigServiceV2Client.php | 4 ++-- src/Logging/V2/LoggingServiceV2Client.php | 6 +++--- src/Monitoring/V3/GroupServiceClient.php | 4 ++-- src/Monitoring/V3/MetricServiceClient.php | 14 +++++++------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Logging/V2/ConfigServiceV2Client.php b/src/Logging/V2/ConfigServiceV2Client.php index ce0166f2f58d..7be1b8aab6e8 100644 --- a/src/Logging/V2/ConfigServiceV2Client.php +++ b/src/Logging/V2/ConfigServiceV2Client.php @@ -534,7 +534,7 @@ public function createSink($parent, $sink, $optionalArgs = []) /** * Updates a sink. If the named sink doesn't exist, then this method is * identical to - * [sinks.create](/logging/docs/api/reference/rest/v2/projects.sinks/create). + * [sinks.create](https://cloud.google.com/logging/docs/api/reference/rest/v2/projects.sinks/create). * If the named sink does exist, then this method replaces the following * fields in the existing sink with values from the new sink: `destination`, * `filter`, `output_version_format`, `start_time`, and `end_time`. @@ -568,7 +568,7 @@ public function createSink($parent, $sink, $optionalArgs = []) * * @type bool $uniqueWriterIdentity * Optional. See - * [sinks.create](/logging/docs/api/reference/rest/v2/projects.sinks/create) + * [sinks.create](https://cloud.google.com/logging/docs/api/reference/rest/v2/projects.sinks/create) * for a description of this field. When updating a sink, the effect of this * field on the value of `writer_identity` in the updated sink depends on both * the old and new values of this field: diff --git a/src/Logging/V2/LoggingServiceV2Client.php b/src/Logging/V2/LoggingServiceV2Client.php index 009fe849853a..4ba615cb0611 100644 --- a/src/Logging/V2/LoggingServiceV2Client.php +++ b/src/Logging/V2/LoggingServiceV2Client.php @@ -402,7 +402,7 @@ public function deleteLog($logName, $optionalArgs = []) * fields. * * To improve throughput and to avoid exceeding the - * [quota limit](/logging/quota-policy) for calls to `entries.write`, + * [quota limit](https://cloud.google.com/logging/quota-policy) for calls to `entries.write`, * you should write multiple log entries at once rather than * calling this method for each individual log entry. * @param array $optionalArgs { @@ -492,7 +492,7 @@ public function writeLogEntries($entries, $optionalArgs = []) /** * Lists log entries. Use this method to retrieve log entries from * Stackdriver Logging. For ways to export log entries, see - * [Exporting Logs](/logging/docs/export). + * [Exporting Logs](https://cloud.google.com/logging/docs/export). * * Sample code: * ``` @@ -535,7 +535,7 @@ public function writeLogEntries($entries, $optionalArgs = []) * `resource_names`. * @type string $filter * Optional. A filter that chooses which log entries to return. See [Advanced - * Logs Filters](/logging/docs/view/advanced_filters). Only log entries that + * Logs Filters](https://cloud.google.com/logging/docs/view/advanced_filters). Only log entries that * match the filter are returned. An empty filter matches all log entries in * the resources listed in `resource_names`. Referencing a parent resource * that is not listed in `resource_names` will cause the filter to return no diff --git a/src/Monitoring/V3/GroupServiceClient.php b/src/Monitoring/V3/GroupServiceClient.php index 27e4f5348945..a076bdf3f8fe 100644 --- a/src/Monitoring/V3/GroupServiceClient.php +++ b/src/Monitoring/V3/GroupServiceClient.php @@ -47,7 +47,7 @@ /** * Service Description: The Group API lets you inspect and manage your - * [groups](google.monitoring.v3.Group). + * [groups][google.monitoring.v3.Group]. * * A group is a named filter that is used to identify * a collection of monitored resources. Groups are typically used to @@ -702,7 +702,7 @@ public function deleteGroup($name, $optionalArgs = []) * 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 optional [list filter](/monitoring/api/learn_more#filtering) describing + * An optional [list filter](https://cloud.google.com/monitoring/api/learn_more#filtering) describing * the members to be returned. The filter may reference the type, labels, and * metadata of monitored resources that comprise the group. * For example, to return only resources representing Compute Engine VM diff --git a/src/Monitoring/V3/MetricServiceClient.php b/src/Monitoring/V3/MetricServiceClient.php index 0b3924722397..1b94a70d0aa5 100644 --- a/src/Monitoring/V3/MetricServiceClient.php +++ b/src/Monitoring/V3/MetricServiceClient.php @@ -414,7 +414,7 @@ public function __construct($options = []) * Optional. * * @type string $filter - * An optional [filter](/monitoring/api/v3/filters) describing + * An optional [filter](https://cloud.google.com/monitoring/api/v3/filters) describing * the descriptors to be returned. The filter can reference * the descriptor's type and labels. For example, the * following filter returns only Google Compute Engine descriptors @@ -560,10 +560,10 @@ public function getMonitoredResourceDescriptor($name, $optionalArgs = []) * @type string $filter * If this field is empty, all custom and * system-defined metric descriptors are returned. - * Otherwise, the [filter](/monitoring/api/v3/filters) + * Otherwise, the [filter](https://cloud.google.com/monitoring/api/v3/filters) * specifies which metric descriptors are to be * returned. For example, the following filter matches all - * [custom metrics](/monitoring/custom-metrics): + * [custom metrics](https://cloud.google.com/monitoring/custom-metrics): * * metric.type = starts_with("custom.googleapis.com/") * @type int $pageSize @@ -674,7 +674,7 @@ public function getMetricDescriptor($name, $optionalArgs = []) /** * Creates a new metric descriptor. * User-created metric descriptors define - * [custom metrics](/monitoring/custom-metrics). + * [custom metrics](https://cloud.google.com/monitoring/custom-metrics). * * Sample code: * ``` @@ -690,7 +690,7 @@ public function getMetricDescriptor($name, $optionalArgs = []) * * @param string $name The project on which to execute the request. The format is * `"projects/{project_id_or_number}"`. - * @param MetricDescriptor $metricDescriptor The new [custom metric](/monitoring/custom-metrics) + * @param MetricDescriptor $metricDescriptor The new [custom metric](https://cloud.google.com/monitoring/custom-metrics) * descriptor. * @param array $optionalArgs { * Optional. @@ -731,7 +731,7 @@ public function createMetricDescriptor($name, $metricDescriptor, $optionalArgs = /** * Deletes a metric descriptor. Only user-created - * [custom metrics](/monitoring/custom-metrics) can be deleted. + * [custom metrics](https://cloud.google.com/monitoring/custom-metrics) can be deleted. * * Sample code: * ``` @@ -813,7 +813,7 @@ public function deleteMetricDescriptor($name, $optionalArgs = []) * * @param string $name The project on which to execute the request. The format is * "projects/{project_id_or_number}". - * @param string $filter A [monitoring filter](/monitoring/api/v3/filters) that specifies which time + * @param string $filter A [monitoring filter](https://cloud.google.com/monitoring/api/v3/filters) that specifies which time * series should be returned. The filter must specify a single metric type, * and can additionally specify metric labels and other information. For * example: From 0e6048c1e606d9439d80a06360e2d4ec1f834b28 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Fri, 3 Mar 2017 15:34:26 -0500 Subject: [PATCH 084/107] [BC Break] Remove paging from Subscription::pull (#375) * remove paging from pull * update system test * docblock updates * fix snippets --- src/PubSub/Subscription.php | 47 +++++++---------- tests/snippets/PubSub/MessageTest.php | 2 +- tests/snippets/PubSub/SubscriptionTest.php | 3 +- tests/system/PubSub/PublishAndPullTest.php | 4 +- tests/unit/PubSub/SubscriptionTest.php | 61 +++------------------- 5 files changed, 30 insertions(+), 87 deletions(-) diff --git a/src/PubSub/Subscription.php b/src/PubSub/Subscription.php index 95608d7d95f9..a956e5b7e8d6 100644 --- a/src/PubSub/Subscription.php +++ b/src/PubSub/Subscription.php @@ -331,42 +331,36 @@ public function reload(array $options = []) * @param array $options [optional] { * Configuration Options * - * @type bool $returnImmediately If set, the system will respond + * @type bool $returnImmediately If true, the system will respond * immediately, even if no messages are available. Otherwise, - * wait until new messages are available. - * @type int $maxMessages Limit the amount of messages pulled. + * wait until new messages are available. **Defaults to** + * `false`. + * @type int $maxMessages Limit the amount of messages pulled. + * **Defaults to** `1000`. * } - * @codingStandardsIgnoreStart - * @return \Generator - * @codingStandardsIgnoreEnd + * @return Message[] */ public function pull(array $options = []) { - $options['pageToken'] = null; + $messages = []; $options['returnImmediately'] = isset($options['returnImmediately']) ? $options['returnImmediately'] : false; - $options['maxMessages'] = isset($options['maxMessages']) ? $options['maxMessages'] : self::MAX_MESSAGES; - do { - $response = $this->connection->pull($options + [ - 'subscription' => $this->name - ]); + $response = $this->connection->pull($options + [ + 'subscription' => $this->name + ]); - if (isset($response['receivedMessages'])) { - foreach ($response['receivedMessages'] as $message) { - yield $this->messageFactory($message, $this->connection, $this->projectId, $this->encode); - } + if (isset($response['receivedMessages'])) { + foreach ($response['receivedMessages'] as $message) { + $messages[] = $this->messageFactory($message, $this->connection, $this->projectId, $this->encode); } + } - // If there's a page token, we'll request the next page. - $options['pageToken'] = isset($response['nextPageToken']) - ? $response['nextPageToken'] - : null; - } while ($options['pageToken']); + return $messages; } /** @@ -378,9 +372,8 @@ public function pull(array $options = []) * Example: * ``` * $messages = $subscription->pull(); - * $messagesArray = iterator_to_array($messages); * - * $subscription->acknowledge($messagesArray[0]); + * $subscription->acknowledge($messages[0]); * ``` * * @codingStandardsIgnoreStart @@ -405,9 +398,8 @@ public function acknowledge(Message $message, array $options = []) * Example: * ``` * $messages = $subscription->pull(); - * $messagesArray = iterator_to_array($messages); * - * $subscription->acknowledgeBatch($messagesArray); + * $subscription->acknowledgeBatch($messages); * ``` * * @codingStandardsIgnoreStart @@ -477,16 +469,15 @@ public function modifyAckDeadline(Message $message, $seconds, array $options = [ * Example: * ``` * $messages = $subscription->pull(); - * $messagesArray = iterator_to_array($messages); * * // Set the ack deadline to three seconds from now for every message - * $subscription->modifyAckDeadlineBatch($messagesArray, 3); + * $subscription->modifyAckDeadlineBatch($messages, 3); * * // Delay execution, or make a sandwich or something. * sleep(2); * * // Now we'll acknowledge - * $subscription->acknowledgeBatch($messagesArray); + * $subscription->acknowledgeBatch($messages); * ``` * * @codingStandardsIgnoreStart diff --git a/tests/snippets/PubSub/MessageTest.php b/tests/snippets/PubSub/MessageTest.php index 3a247c8a4c64..c8991e5fc87f 100644 --- a/tests/snippets/PubSub/MessageTest.php +++ b/tests/snippets/PubSub/MessageTest.php @@ -85,7 +85,7 @@ public function testClass() ); $res = $snippet->invoke('messages'); - $this->assertInstanceOf(\Generator::class, $res->returnVal()); + $this->assertContainsOnlyInstancesOf(Message::class, $res->returnVal()); $this->assertEquals('hello world', $res->output()); } diff --git a/tests/snippets/PubSub/SubscriptionTest.php b/tests/snippets/PubSub/SubscriptionTest.php index e877fa061a3c..84cefa0ce3a7 100644 --- a/tests/snippets/PubSub/SubscriptionTest.php +++ b/tests/snippets/PubSub/SubscriptionTest.php @@ -21,6 +21,7 @@ use Google\Cloud\Dev\Snippet\SnippetTestCase; use Google\Cloud\Iam\Iam; use Google\Cloud\PubSub\Connection\ConnectionInterface; +use Google\Cloud\PubSub\Message; use Google\Cloud\PubSub\PubSubClient; use Google\Cloud\PubSub\Subscription; use Prophecy\Argument; @@ -169,7 +170,7 @@ public function testPull() $this->subscription->setConnection($this->connection->reveal()); $res = $snippet->invoke('messages'); - $this->assertInstanceOf(\Generator::class, $res->returnVal()); + $this->assertContainsOnlyInstancesOf(Message::class, $res->returnVal()); $this->assertEquals('hello world', $res->output()); } diff --git a/tests/system/PubSub/PublishAndPullTest.php b/tests/system/PubSub/PublishAndPullTest.php index e8347eb1af41..61e3df1bf720 100644 --- a/tests/system/PubSub/PublishAndPullTest.php +++ b/tests/system/PubSub/PublishAndPullTest.php @@ -42,7 +42,7 @@ public function testPublishMessageAndPull($client) ]; $topic->publish($message); - $messages = iterator_to_array($sub->pull()); + $messages = $sub->pull(); $sub->modifyAckDeadline($messages[0], 15); $sub->acknowledge($messages[0]); @@ -79,7 +79,7 @@ public function testPublishMessagesAndPull($client) $topic->publishBatch($messages); - $actualMessages = iterator_to_array($sub->pull()); + $actualMessages = $sub->pull(); $sub->modifyAckDeadlineBatch($actualMessages, 15); $sub->acknowledgeBatch($actualMessages); diff --git a/tests/unit/PubSub/SubscriptionTest.php b/tests/unit/PubSub/SubscriptionTest.php index 3dda439663a3..d6cb96a05c40 100644 --- a/tests/unit/PubSub/SubscriptionTest.php +++ b/tests/unit/PubSub/SubscriptionTest.php @@ -199,11 +199,9 @@ public function testPull() 'foo' => 'bar' ]); - $this->assertInstanceOf(Generator::class, $result); - - $arr = iterator_to_array($result); - $this->assertInstanceOf(Message::class, $arr[0]); - $this->assertInstanceOf(Message::class, $arr[1]); + $this->assertContainsOnlyInstancesOf(Message::class, $result); + $this->assertInstanceOf(Message::class, $result[0]); + $this->assertInstanceOf(Message::class, $result[1]); } public function testPullWithCustomArgs() @@ -235,56 +233,9 @@ public function testPullWithCustomArgs() 'maxMessages' => 2 ]); - $this->assertInstanceOf(Generator::class, $result); - - $arr = iterator_to_array($result); - $this->assertInstanceOf(Message::class, $arr[0]); - $this->assertInstanceOf(Message::class, $arr[1]); - } - - public function testPullPaged() - { - $messages = [ - 'receivedMessages' => [ - [ - 'message' => [] - ], [ - 'message' => [] - ] - ], - 'nextPageToken' => 'foo' - ]; - - $this->connection->pull(Argument::that(function ($args) { - if ($args['foo'] !== 'bar') return false; - if ($args['returnImmediately'] !== true) return false; - if ($args['maxMessages'] !== 2) return false; - if (!in_array($args['pageToken'], [null, 'foo'])) return false; - - return true; - }))->willReturn($messages) - ->shouldBeCalledTimes(3); - - $this->subscription->setConnection($this->connection->reveal()); - - $result = $this->subscription->pull([ - 'foo' => 'bar', - 'returnImmediately' => true, - 'maxMessages' => 2 - ]); - - $this->assertInstanceOf(Generator::class, $result); - - // enumerate the iterator and kill after it loops twice. - $arr = []; - $i = 0; - foreach ($result as $message) { - $i++; - $arr[] = $message; - if ($i == 6) break; - } - - $this->assertEquals(6, count($arr)); + $this->assertContainsOnlyInstancesOf(Message::class, $result); + $this->assertInstanceOf(Message::class, $result[0]); + $this->assertInstanceOf(Message::class, $result[1]); } public function testAcknowledge() From 32bd57ea7bd52adf9cda6805b83836643c3cd4fa Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Fri, 3 Mar 2017 17:24:01 -0500 Subject: [PATCH 085/107] Ignore private methods on snippet test coverage (#380) Also, if snippets are uncovered, the suite will fail. --- .travis.yml | 3 +++ dev/src/Snippet/Parser/Parser.php | 4 ++++ tests/snippets/bootstrap.php | 1 + 3 files changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 01f68113bc39..fd8876ac630c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,3 +25,6 @@ script: after_success: - bash <(curl -s https://codecov.io/bash) - ./dev/sh/push-docs + +after_failure: + - echo "SNIPPET COVERAGE REPORT" && cat ./build/snippets-uncovered.json diff --git a/dev/src/Snippet/Parser/Parser.php b/dev/src/Snippet/Parser/Parser.php index c2ec03f75674..06007b9817d1 100644 --- a/dev/src/Snippet/Parser/Parser.php +++ b/dev/src/Snippet/Parser/Parser.php @@ -176,6 +176,10 @@ public function examplesFromMethod($class, $method) $method = new ReflectionMethod($class, $method); } + if (!$method->isPublic()) { + return []; + } + $doc = new DocBlock($method); $parent = $method->getDeclaringClass(); diff --git a/tests/snippets/bootstrap.php b/tests/snippets/bootstrap.php index d6448ffe6885..b8c3c9ef2e53 100644 --- a/tests/snippets/bootstrap.php +++ b/tests/snippets/bootstrap.php @@ -30,6 +30,7 @@ if (!empty($uncovered)) { echo sprintf("\033[31mNOTICE: %s uncovered snippets! See build/snippets-uncovered.json for a report.\n", count($uncovered)); + exit(1); } }); From a3965b3c2dab50caedd8ad6aec3ec8aa576a165b Mon Sep 17 00:00:00 2001 From: David Supplee Date: Mon, 6 Mar 2017 12:44:10 -0500 Subject: [PATCH 086/107] fix outdated links (#383) --- src/Logging/Entry.php | 4 +--- src/Logging/Logger.php | 16 +++++++-------- src/Logging/LoggingClient.php | 8 ++------ src/Logging/PsrLogger.php | 8 +++----- src/NaturalLanguage/Annotation.php | 20 ++++++++----------- src/NaturalLanguage/NaturalLanguageClient.php | 8 ++++---- 6 files changed, 25 insertions(+), 39 deletions(-) diff --git a/src/Logging/Entry.php b/src/Logging/Entry.php index 0d3a9293b692..c20b161f4697 100644 --- a/src/Logging/Entry.php +++ b/src/Logging/Entry.php @@ -58,9 +58,7 @@ public function __construct(array $info = []) * echo $info['textPayload']; * ``` * - * @codingStandardsIgnoreStart - * @see https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry LogEntry resource documentation. - * @codingStandardsIgnoreEnd + * @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry LogEntry resource documentation. * * @return array */ diff --git a/src/Logging/Logger.php b/src/Logging/Logger.php index 2e0735c9cce0..ac77e9bfc57c 100644 --- a/src/Logging/Logger.php +++ b/src/Logging/Logger.php @@ -100,17 +100,15 @@ class Logger private $labels; /** - * @codingStandardsIgnoreStart * @param ConnectionInterface $connection Represents a connection to * Stackdriver Logging. * @param string $name The name of the log to write entries to. * @param string $projectId The project's ID. * @param array $resource [optional] The - * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource) + * [monitored resource](https://cloud.google.com/logging/docs/reference/v2/rest/v2/MonitoredResource) * to associate log entries with. **Defaults to** type global. * @param array $labels [optional] A set of user-defined (key, value) data * that provides additional information about the log entries. - * @codingStandardsIgnoreEnd */ public function __construct( ConnectionInterface $connection, @@ -242,8 +240,8 @@ public function entries(array $options = []) * ] * ]); * ``` - * @codingStandardsIgnoreStart - * @see https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry LogEntry resource documentation. + * + * @see https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry LogEntry resource documentation. * * @param array|string $data The data to log. When providing a string the * data will be stored as a `textPayload` type. When providing an @@ -252,24 +250,24 @@ public function entries(array $options = []) * Configuration options. * * @type array $resource The - * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource) + * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource) * to associate this log entry with. **Defaults to** type global. * @type array $httpRequest Information about the HTTP request * associated with this log entry, if applicable. Please see - * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry#httprequest) + * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#httprequest) * for more information. * @type array $labels A set of user-defined (key, value) data that * provides additional information about the log entry. * @type array $operation Additional information about a potentially * long-running operation with which a log entry is associated. - * Please see [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry#logentryoperation) + * Please see + * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#logentryoperation) * for more information. * @type string|int $severity The severity of the log entry. **Defaults to** * `"DEFAULT"`. * } * @return Entry * @throws \InvalidArgumentException - * @codingStandardsIgnoreEnd */ public function entry($data, array $options = []) { diff --git a/src/Logging/LoggingClient.php b/src/Logging/LoggingClient.php index 0eec574c0cc9..b102bc61935a 100644 --- a/src/Logging/LoggingClient.php +++ b/src/Logging/LoggingClient.php @@ -412,7 +412,6 @@ public function entries(array $options = []) * $psrLogger = $logging->psrLogger('my-log'); * ``` * - * @codingStandardsIgnoreStart * @param string $name The name of the log to write entries to. * @param array $options [optional] { * Configuration options. @@ -420,13 +419,12 @@ public function entries(array $options = []) * @type string $messageKey The key in the `jsonPayload` used to contain * the logged message. **Defaults to** `message`. * @type array $resource The - * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource) + * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource) * to associate log entries with. **Defaults to** type global. * @type array $labels A set of user-defined (key, value) data that * provides additional information about the log entry. * } * @return PsrLogger - * @codingStandardsIgnoreEnd */ public function psrLogger($name, array $options = []) { @@ -450,18 +448,16 @@ public function psrLogger($name, array $options = []) * $logger = $logging->logger('my-log'); * ``` * - * @codingStandardsIgnoreStart * @param string $name The name of the log to write entries to. * @param array $options [optional] { * Configuration options. * * @type array $resource The - * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource) + * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource) * to associate log entries with. **Defaults to** type global. * @type array $labels A set of user-defined (key, value) data that * provides additional information about the log entry. * } - * @codingStandardsIgnoreEnd * @return Logger */ public function logger($name, array $options = []) diff --git a/src/Logging/PsrLogger.php b/src/Logging/PsrLogger.php index 19a2bfc5636d..684a5af050a9 100644 --- a/src/Logging/PsrLogger.php +++ b/src/Logging/PsrLogger.php @@ -227,7 +227,6 @@ public function debug($message, array $context = []) * ]); * ``` * - * @codingStandardsIgnoreStart * @param string|int $level The severity of the log entry. * @param string $message The message to log. * @param array $context { @@ -239,12 +238,12 @@ public function debug($message, array $context = []) * Stackdriver specific data. * * @type array $stackdriverOptions['resource'] The - * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/MonitoredResource) + * [monitored resource](https://cloud.google.com/logging/docs/api/reference/rest/v2/MonitoredResource) * to associate this log entry with. **Defaults to** type global. * @type array $stackdriverOptions['httpRequest'] Information about the * HTTP request associated with this log entry, if applicable. * Please see - * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry#httprequest) + * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#httprequest) * for more information. * @type array $stackdriverOptions['labels'] A set of user-defined * (key, value) data that provides additional information about @@ -252,11 +251,10 @@ public function debug($message, array $context = []) * @type array $stackdriverOptions['operation'] Additional information * about a potentially long-running operation with which a log * entry is associated. Please see - * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/Shared.Types/LogEntry#logentryoperation) + * [the API docs](https://cloud.google.com/logging/docs/api/reference/rest/v2/LogEntry#logentryoperation) * for more information. * } * @throws InvalidArgumentException - * @codingStandardsIgnoreEnd */ public function log($level, $message, array $context = []) { diff --git a/src/NaturalLanguage/Annotation.php b/src/NaturalLanguage/Annotation.php index 3e0cdd19aaf2..0d1616b7b248 100644 --- a/src/NaturalLanguage/Annotation.php +++ b/src/NaturalLanguage/Annotation.php @@ -54,9 +54,7 @@ * } * ``` * - * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#Sentence Sentence type documentation - * @codingStandardsIgnoreEnd + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Sentence Sentence type documentation * * @return array|null * } @@ -70,9 +68,7 @@ * } * ``` * - * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#Token Token type documentation - * @codingStandardsIgnoreEnd + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Token Token type documentation * * @return array|null * } @@ -86,7 +82,7 @@ * } * ``` * - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/Entity Entity type documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Entity Entity type documentation * * @return array|null * } @@ -129,7 +125,7 @@ public function __construct(array $info = []) * ``` * * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#response-body Annotate Text documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/annotateText#response-body Annotate Text documentation * @codingStandardsIgnoreEnd * * @return array @@ -151,7 +147,7 @@ public function info() * } * ``` * - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/Sentiment Sentiment type documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Sentiment Sentiment type documentation * * @return array|null */ @@ -173,7 +169,7 @@ public function sentiment() * ``` * * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#tag Token tags documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Token#Tag Token tags documentation * @codingStandardsIgnoreEnd * * @return array|null @@ -200,7 +196,7 @@ public function tokensByTag($tag) * ``` * * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText#label Token labels documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Token#Label Token labels documentation * @codingStandardsIgnoreEnd * * @return array|null @@ -227,7 +223,7 @@ public function tokensByLabel($label) * ``` * * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/Entity#type Entity types documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Entity#Type Entity types documentation * @codingStandardsIgnoreEnd * * @return array|null diff --git a/src/NaturalLanguage/NaturalLanguageClient.php b/src/NaturalLanguage/NaturalLanguageClient.php index 61c767e87084..51ad1db931a8 100644 --- a/src/NaturalLanguage/NaturalLanguageClient.php +++ b/src/NaturalLanguage/NaturalLanguageClient.php @@ -117,7 +117,7 @@ public function __construct(array $config = []) * ``` * * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/analyzeEntities Analyze Entities API documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeEntities Analyze Entities API documentation * @codingStandardsIgnoreEnd * * @param string|StorageObject $content The content to analyze. @@ -159,7 +159,7 @@ public function analyzeEntities($content, array $options = []) * ``` * * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/analyzeSentiment Analyze Sentiment API documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeSentiment Analyze Sentiment API documentation * @codingStandardsIgnoreEnd * * @param string|StorageObject $content The content to analyze. @@ -200,7 +200,7 @@ public function analyzeSentiment($content, array $options = []) * ``` * * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/analyzeSyntax Analyze Syntax API documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeSyntax Analyze Syntax API documentation * @codingStandardsIgnoreEnd * * @param string|StorageObject $content The content to analyze. @@ -253,7 +253,7 @@ public function analyzeSyntax($content, array $options = []) * ``` * * @codingStandardsIgnoreStart - * @see https://cloud.google.com/natural-language/reference/rest/v1beta1/documents/annotateText Annotate Text API documentation + * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/annotateText Annotate Text API documentation * @codingStandardsIgnoreEnd * * @param string|StorageObject $content The content to annotate. From 17e6282817ac43c8661c6c29d6641ca43fbea690 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Mon, 6 Mar 2017 16:49:55 -0500 Subject: [PATCH 087/107] doc hygeine (#385) --- src/BigQuery/BigQueryClient.php | 4 +- src/Datastore/DatastoreClient.php | 12 ++-- src/Logging/LoggingClient.php | 6 +- src/NaturalLanguage/NaturalLanguageClient.php | 4 +- src/PubSub/PubSubClient.php | 4 +- src/ServiceBuilder.php | 59 ++++++++++--------- src/Speech/SpeechClient.php | 7 +-- src/Storage/StorageClient.php | 4 +- src/Translate/TranslateClient.php | 22 +++---- src/Vision/VisionClient.php | 2 +- 10 files changed, 64 insertions(+), 60 deletions(-) diff --git a/src/BigQuery/BigQueryClient.php b/src/BigQuery/BigQueryClient.php index 7daae138f322..07d771be5a65 100644 --- a/src/BigQuery/BigQueryClient.php +++ b/src/BigQuery/BigQueryClient.php @@ -26,8 +26,8 @@ use Psr\Http\Message\StreamInterface; /** - * Google Cloud BigQuery client. Allows you to create, manage, share and query - * data. Find more information at + * Google Cloud BigQuery allows you to create, manage, share and query data. + * Find more information at the * [Google Cloud BigQuery Docs](https://cloud.google.com/bigquery/what-is-bigquery). * * Example: diff --git a/src/Datastore/DatastoreClient.php b/src/Datastore/DatastoreClient.php index bab400e6a48f..bbfcc6ebd98c 100644 --- a/src/Datastore/DatastoreClient.php +++ b/src/Datastore/DatastoreClient.php @@ -29,13 +29,15 @@ use Psr\Cache\CacheItemPoolInterface; /** - * Google Cloud Datastore client. Cloud Datastore is a highly-scalable NoSQL - * database for your applications. Find more information at + * Google Cloud Datastore is a highly-scalable NoSQL database for your + * applications. Find more information at the * [Google Cloud Datastore docs](https://cloud.google.com/datastore/docs/). * - * Cloud Datastore supports [multi-tenant](https://cloud.google.com/datastore/docs/concepts/multitenancy) applications - * through use of data partitions. A partition ID can be supplied when creating an instance of Cloud Datastore, and will - * be used in all operations executed in that instance. + * Cloud Datastore supports + * [multi-tenant](https://cloud.google.com/datastore/docs/concepts/multitenancy) + * applications through use of data partitions. A partition ID can be supplied + * when creating an instance of Cloud Datastore, and will be used in all + * operations executed in that instance. * * To enable the * [Google Cloud Datastore Emulator](https://cloud.google.com/datastore/docs/tools/datastore-emulator), diff --git a/src/Logging/LoggingClient.php b/src/Logging/LoggingClient.php index b102bc61935a..ca537f2fde4a 100644 --- a/src/Logging/LoggingClient.php +++ b/src/Logging/LoggingClient.php @@ -24,9 +24,9 @@ use Psr\Cache\CacheItemPoolInterface; /** - * Google Stackdriver Logging client. Allows you to store, search, analyze, - * monitor, and alert on log data and events from Google Cloud Platform and - * Amazon Web Services. Find more information at + * Google Stackdriver Logging allows you to store, search, analyze, monitor, and + * alert on log data and events from Google Cloud Platform and Amazon Web + * Services. Find more information at the * [Google Stackdriver Logging docs](https://cloud.google.com/logging/docs/). * * This client supports transport over diff --git a/src/NaturalLanguage/NaturalLanguageClient.php b/src/NaturalLanguage/NaturalLanguageClient.php index 51ad1db931a8..3861c096a02a 100644 --- a/src/NaturalLanguage/NaturalLanguageClient.php +++ b/src/NaturalLanguage/NaturalLanguageClient.php @@ -24,10 +24,10 @@ use Psr\Cache\CacheItemPoolInterface; /** - * Google Cloud Natural Language client. Provides natural language understanding + * Google Cloud Natural Language provides natural language understanding * technologies to developers, including sentiment analysis, entity recognition, * and syntax analysis. Currently only English, Spanish, and Japanese textual - * context are supported. Find more information at + * context are supported. Find more information at the * [Google Cloud Natural Language docs](https://cloud.google.com/natural-language/docs/). * * Example: diff --git a/src/PubSub/PubSubClient.php b/src/PubSub/PubSubClient.php index 9e539b09f508..774495fcd48f 100644 --- a/src/PubSub/PubSubClient.php +++ b/src/PubSub/PubSubClient.php @@ -24,8 +24,8 @@ use Psr\Cache\CacheItemPoolInterface; /** - * Google Cloud Pub/Sub client. Allows you to send and receive - * messages between independent applications. Find more information at + * Google Cloud Pub/Sub allows you to send and receive + * messages between independent applications. Find more information at the * [Google Cloud Pub/Sub docs](https://cloud.google.com/pubsub/docs/). * * To enable the [Google Cloud Pub/Sub Emulator](https://cloud.google.com/pubsub/emulator), diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index 232ae57e5c6e..d6eada64d808 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -97,8 +97,8 @@ public function __construct(array $config = []) } /** - * Google Cloud BigQuery client. Allows you to create, manage, share and query - * data. Find more information at + * Google Cloud BigQuery allows you to create, manage, share and query + * data. Find more information at the * [Google Cloud BigQuery Docs](https://cloud.google.com/bigquery/what-is-bigquery). * * Example: @@ -121,9 +121,9 @@ public function bigQuery(array $config = []) } /** - * Google Cloud Datastore client. Cloud Datastore is a highly-scalable NoSQL - * database for your applications. Find more information at - * [Google Cloud Datastore docs](https://cloud.google.com/datastore/docs/). + * Google Cloud Datastore is a highly-scalable NoSQL database for your + * applications. Find more information at the + * Google Cloud Datastore docs](https://cloud.google.com/datastore/docs/). * * Example: * ``` @@ -145,9 +145,9 @@ public function datastore(array $config = []) } /** - * Google Stackdriver Logging client. Allows you to store, search, analyze, - * monitor, and alert on log data and events from Google Cloud Platform and - * Amazon Web Services. Find more information at + * Google Stackdriver Logging allows you to store, search, analyze, monitor, + * and alert on log data and events from Google Cloud Platform and Amazon + * Web Services. Find more information at the * [Google Stackdriver Logging docs](https://cloud.google.com/logging/docs/). * * Example: @@ -165,10 +165,10 @@ public function logging(array $config = []) } /** - * Google Cloud Natural Language client. Provides natural language - * understanding technologies to developers, including sentiment analysis, - * entity recognition, and syntax analysis. Currently only English, Spanish, - * and Japanese textual context are supported. Find more information at + * Google Cloud Natural Language provides natural language understanding + * technologies to developers, including sentiment analysis, entity + * recognition, and syntax analysis. Currently only English, Spanish, + * and Japanese textual context are supported. Find more information at the * [Google Cloud Natural Language docs](https://cloud.google.com/natural-language/docs/). * * Example: @@ -186,8 +186,8 @@ public function naturalLanguage(array $config = []) } /** - * Google Cloud Pub/Sub client. Allows you to send and receive - * messages between independent applications. Find more information at + * Google Cloud Pub/Sub allows you to send and receive messages between + * independent applications. Find more information at the * [Google Cloud Pub/Sub docs](https://cloud.google.com/pubsub/docs/). * * Example: @@ -210,11 +210,10 @@ public function pubsub(array $config = []) } /** - * Google Cloud Speech client. Enables easy integration of Google speech - * recognition technologies into developer applications. Send audio and - * receive a text transcription from the Cloud Speech API service. Find more - * information at - * [Google Cloud Speech API docs](https://developers.google.com/speech). + * Google Cloud Speech enables easy integration of Google speech recognition + * technologies into developer applications. Send audio and receive a text + * transcription from the Cloud Speech API service. Find more information at + * the [Google Cloud Speech API docs](https://cloud.google.com/speech/docs/). * * Example: * ``` @@ -231,8 +230,8 @@ public function speech(array $config = []) } /** - * Google Cloud Storage client. Allows you to store and retrieve data on - * Google's infrastructure. Find more information at + * Google Cloud Storage allows you to store and retrieve data on Google's + * infrastructure. Find more information at the * [Google Cloud Storage API docs](https://developers.google.com/storage). * * Example: @@ -250,9 +249,10 @@ public function storage(array $config = []) } /** - * Google Cloud Vision client. Allows you to understand the content of an - * image, classify images into categories, detect text, objects, faces and - * more. Find more information at [Google Cloud Vision docs](https://cloud.google.com/vision/docs/). + * Google Cloud Vision allows you to understand the content of an image, + * classify images into categories, detect text, objects, faces and more. + * Find more information at the + * [Google Cloud Vision docs](https://cloud.google.com/vision/docs/). * * Example: * ``` @@ -269,13 +269,14 @@ public function vision(array $config = []) } /** - * Google Translate client. Provides the ability to dynamically - * translate text between thousands of language pairs and lets websites and - * programs integrate with the Google Cloud Translation API - * programmatically. The Google Cloud Translation API is available as a paid + * Google Cloud Translation provides the ability to dynamically translate + * text between thousands of language pairs and lets websites and programs + * integrate with translation service programmatically. + * + * The Google Cloud Translation API is available as a paid * service. See the [Pricing](https://cloud.google.com/translation/v2/pricing) * and [FAQ](https://cloud.google.com/translation/v2/faq) pages for details. - * Find more information at the + * Find more information at the the * [Google Cloud Translation docs](https://cloud.google.com/translation/docs/). * * Please note that while the Google Cloud Translation API supports diff --git a/src/Speech/SpeechClient.php b/src/Speech/SpeechClient.php index 358211c820d5..7bb2e2108f86 100644 --- a/src/Speech/SpeechClient.php +++ b/src/Speech/SpeechClient.php @@ -24,10 +24,9 @@ use Psr\Cache\CacheItemPoolInterface; /** - * Google Cloud Speech client. The Cloud Speech API enables easy integration of - * Google speech recognition technologies into developer applications. Send - * audio and receive a text transcription from the Cloud Speech API service. - * Find more information at the + * Google Cloud Speech enables easy integration of Google speech recognition + * technologies into developer applications. Send audio and receive a text + * transcription from the Cloud Speech API service. Find more information at the * [Google Cloud Speech docs](https://cloud.google.com/speech/docs/). * * To enable better detection of encoding/sample rate values it is recommended diff --git a/src/Storage/StorageClient.php b/src/Storage/StorageClient.php index 0caa41313d39..0afcdb8ac919 100644 --- a/src/Storage/StorageClient.php +++ b/src/Storage/StorageClient.php @@ -23,8 +23,8 @@ use Psr\Cache\CacheItemPoolInterface; /** - * Google Cloud Storage client. Allows you to store and retrieve data on - * Google's infrastructure. Find more information at + * Google Cloud Storage allows you to store and retrieve data on Google's + * infrastructure. Find more information at the * [Google Cloud Storage API docs](https://developers.google.com/storage). * * Example: diff --git a/src/Translate/TranslateClient.php b/src/Translate/TranslateClient.php index 942f1d41efa1..0d44a76c5652 100644 --- a/src/Translate/TranslateClient.php +++ b/src/Translate/TranslateClient.php @@ -22,19 +22,21 @@ use Google\Cloud\Translate\Connection\Rest; /** - * Google Translate client. Provides the ability to dynamically - * translate text between thousands of language pairs and lets websites and - * programs integrate with the Google Cloud Translation API programmatically. - * The Google Cloud Translation API is available as a paid service. See the - * [Pricing](https://cloud.google.com/translation/v2/pricing) and - * [FAQ](https://cloud.google.com/translation/v2/faq) pages for details. Find - * more information at the + * Google Cloud Translation provides the ability to dynamically translate + * text between thousands of language pairs and lets websites and programs + * integrate with translation service programmatically. Find more + * information at the the * [Google Cloud Translation docs](https://cloud.google.com/translation/docs/). * + * The Google Cloud Translation API is available as a paid + * service. See the [Pricing](https://cloud.google.com/translation/v2/pricing) + * and [FAQ](https://cloud.google.com/translation/v2/faq) pages for details. + * * Please note that while the Google Cloud Translation API supports - * authentication via service account and application default credentials like - * other Cloud Platform APIs, it also supports authentication via a public API - * access key. If you wish to authenticate using an API key, follow the + * authentication via service account and application default credentials + * like other Cloud Platform APIs, it also supports authentication via a + * public API access key. If you wish to authenticate using an API key, + * follow the * [before you begin](https://cloud.google.com/translation/v2/translating-text-with-rest#before-you-begin) * instructions to learn how to generate a key. * diff --git a/src/Vision/VisionClient.php b/src/Vision/VisionClient.php index 3ee0eb437477..b7d203bdb38f 100644 --- a/src/Vision/VisionClient.php +++ b/src/Vision/VisionClient.php @@ -26,7 +26,7 @@ /** * Google Cloud Vision allows you to understand the content of an image, * classify images into categories, detect text, objects, faces and more. Find - * more information at + * more information at the * [Google Cloud Vision docs](https://cloud.google.com/vision/docs/). * * Example: From 3c2c02d757830d8850dcc26e4ebbeccca8e6dbc8 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Tue, 7 Mar 2017 12:52:19 -0500 Subject: [PATCH 088/107] Add GAPIC docs to docs site (#373) * Add gapic clients to docs * Link to protos, gax, auth * Parse `@see` tags for external classes * Add external ref * Add the rest of the external classes --- dev/src/DocGenerator/Parser/CodeParser.php | 44 ++++++++++-- docs/external-classes.json | 83 ++++++++++++++++++++++ docs/toc.json | 63 ++++++++++++++++ src/ErrorReporting/README.md | 5 ++ src/ErrorReporting/V1beta1/README.md | 5 ++ src/Logging/V2/README.md | 5 ++ src/Monitoring/README.md | 5 ++ src/Monitoring/V3/README.md | 5 ++ src/PubSub/V1/README.md | 5 ++ src/Speech/V1beta1/README.md | 5 ++ 10 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 docs/external-classes.json create mode 100644 src/ErrorReporting/README.md create mode 100644 src/ErrorReporting/V1beta1/README.md create mode 100644 src/Logging/V2/README.md create mode 100644 src/Monitoring/README.md create mode 100644 src/Monitoring/V3/README.md create mode 100644 src/PubSub/V1/README.md create mode 100644 src/Speech/V1beta1/README.md diff --git a/dev/src/DocGenerator/Parser/CodeParser.php b/dev/src/DocGenerator/Parser/CodeParser.php index cbc6616b461e..e478bdd41d42 100644 --- a/dev/src/DocGenerator/Parser/CodeParser.php +++ b/dev/src/DocGenerator/Parser/CodeParser.php @@ -31,6 +31,7 @@ class CodeParser implements ParserInterface private $outputName; private $reflector; private $markdown; + private $externalTypes; public function __construct($path, $outputName, FileReflector $reflector) { @@ -38,6 +39,7 @@ public function __construct($path, $outputName, FileReflector $reflector) $this->outputName = $outputName; $this->reflector = $reflector; $this->markdown = \Parsedown::instance(); + $this->externalTypes = json_decode(file_get_contents(__DIR__ .'/../../../../docs/external-classes.json'), true); } public function parse() @@ -120,8 +122,11 @@ private function buildDescription($docBlock, $content = null) foreach ($parsedContents as &$part) { if ($part instanceof Seetag) { $reference = $part->getReference(); + if (substr_compare($reference, 'Google\Cloud', 0, 12) === 0) { $part = $this->buildLink($reference); + } elseif ($this->hasExternalType(trim(str_replace('@see', '', $part)))) { + $part = $this->buildExternalType(trim(str_replace('@see', '', $part))); } } } @@ -427,14 +432,10 @@ private function buildReturns($returns) private function handleTypes($types) { foreach ($types as &$type) { - // object is a PHPDoc keyword so it is not capable of detecting the context - // https://github.com/phpDocumentor/ReflectionDocBlock/blob/2.0.4/src/phpDocumentor/Reflection/DocBlock/Type/Collection.php#L37 - if ($type === 'Object') { - $type = '\Google\Cloud\Storage\Object'; - } - if (substr_compare($type, '\Google\Cloud', 0, 13) === 0) { $type = $this->buildLink($type); + } elseif ($this->hasExternalType($type)) { + $type = $this->buildExternalType($type); } $matches = []; @@ -451,6 +452,37 @@ private function handleTypes($types) return $types; } + private function hasExternalType($type) + { + $type = trim($type, '\\'); + $types = array_filter($this->externalTypes, function ($external) use ($type) { + return (strpos($type, $external['name']) !== false); + }); + + if (count($types) === 0) { + return false; + } + + return true; + } + + private function buildExternalType($type) + { + $type = trim($type, '\\'); + $types = array_values(array_filter($this->externalTypes, function ($external) use ($type) { + return (strpos($type, $external['name']) !== false); + })); + + $external = $types[0]; + + $href = sprintf($external['uri'], str_replace($external['name'], '', $type)); + return sprintf( + '%s', + $href, + $type + ); + } + private function buildLink($content) { if ($content[0] === '\\') { diff --git a/docs/external-classes.json b/docs/external-classes.json new file mode 100644 index 000000000000..580897786b30 --- /dev/null +++ b/docs/external-classes.json @@ -0,0 +1,83 @@ +[{ + "name": "Google\\Auth\\", + "uri": "https://github.com/google/google-auth-library-php/blob/master/src/%s.php" +}, { + "name": "Google\\GAX\\", + "uri": "https://github.com/googleapis/gax-php/tree/master/src/%s.php" +}, { + "name": "google\\iam\\v1\\", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/iam/v1" +}, { + "name": "google\\devtools\\clouderrorreporting\\v1beta1\\", + "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/errorreporting/v1beta1" +}, { + "name": "google\\cloud\\language\\v1\\", + "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/language/v1" +}, { + "name": "google\\logging\\v2\\", + "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/logging/v2" +}, { + "name": "google\\monitoring\\v3\\", + "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/monitoring/v3" +}, { + "name": "google\\pubsub\\v1\\", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/pubsub/v1/pubsub.php" +}, { + "name": "google\\spanner\\admin\\database\\v1\\", + "name": "https://github.com/googleapis/proto-client-php/blob/master/src/spanner/admin/database/v1/spanner_database_admin.php" +}, { + "name": "google\\spanner\\admin\\instance\\v1\\", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/spanner/admin/instance/v1/spanner_instance_admin.php" +}, { + "name": "google\\spanner\\v1\\", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/spanner/v1" +}, { + "name": "google\\cloud\\speech\\v1beta1\\", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/speech/v1beta1/cloud_speech.php" +}, { + "name": "google\\devtools\\cloudtrace\\v1\\", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/trace/v1/trace.php" +}, { + "name": "google\\cloud\\vision\\v1\\", + "uri": "https://github.com/googleapis/proto-client-php/tree/master/src/vision/v1" +}, { + "name": "google\\longrunning\\", + "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/operations.php" +}, { + "name": "MonitoredResource", + "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/monitored_resource.php" +}, { + "name": "ServiceContextFilter", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/errorreporting/v1beta1/error_stats_service.php#L1985" +}, { + "name": "QueryTimeRange", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/errorreporting/v1beta1/error_stats_service.php#L1915" +}, { + "name": "Duration", + "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/well-known-types/duration.php" +}, { + "name": "TimedCountAlignment", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/errorreporting/v1beta1/error_stats_service.php#L8" +}, { + "name": "Timestamp", + "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/well-known-types/timestamp.php" +}, { + "name": "ErrorGroupOrder", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/errorreporting/v1beta1/error_stats_service.php#L16" +}, { + "name": "TimeInterval", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/monitoring/v3/common.php#L270" +}, { + "name": "google\\api\\MetricDescriptor", + "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/metric.php#L29" +}, { + "name": "google\\api\\MonitoredResourceDescriptor", + "uri": "https://github.com/googleapis/gax-php/blob/master/src/generated/monitored_resource.php#L8" +}, { + "name": "Aggregation", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/monitoring/v3/common.php#L429" +}, { + "name": "PushConfig", + "uri": "https://github.com/googleapis/proto-client-php/blob/master/src/pubsub/v1/pubsub.php#L1827" +}] + diff --git a/docs/toc.json b/docs/toc.json index 2585883aafbb..b61475532810 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -93,6 +93,24 @@ "type": "datastore/blob" }] }, + { + "title": "Error Reporting", + "type": "errorreporting/readme", + "nav": [{ + "title": "v1beta1", + "type": "errorreporting/v1beta1/readme", + "nav": [{ + "title": "ErrorGroupServiceClient", + "type": "errorreporting/v1beta1/errorgroupserviceclient" + }, { + "title": "ErrorStatsServiceClient", + "type": "errorreporting/v1beta1/errorstatsserviceclient" + }, { + "title": "ReportErrorsServiceClient", + "type": "errorreporting/v1beta1/reporterrorsserviceclient" + }] + }] + }, { "title": "Logging", "type": "logging/loggingclient", @@ -111,6 +129,34 @@ },{ "title": "Sink", "type": "logging/sink" + }, { + "title": "v2", + "type": "logging/v2/readme", + "nav": [{ + "title": "ConfigServiceV2Client", + "type": "logging/v2/configservicev2client" + }, { + "title": "LoggingServiceV2Client", + "type": "logging/v2/loggingservicev2client" + }, { + "title": "MetricsServiceV2Client", + "type": "logging/v2/metricsservicev2client" + }] + }] + }, + { + "title": "Monitoring", + "type": "monitoring/readme", + "nav": [{ + "title": "v3", + "type": "monitoring/v3/readme", + "nav": [{ + "title": "GroupServiceClient", + "type": "monitoring/v3/groupserviceclient" + }, { + "title": "MetricServiceClient", + "type": "monitoring/v3/metricserviceclient" + }] }] }, { @@ -135,6 +181,16 @@ { "title": "Topic", "type": "pubsub/topic" + }, { + "title": "v1", + "type": "pubsub/v1/readme", + "nav": [{ + "title": "PublisherClient", + "type": "pubsub/v1/publisherclient" + }, { + "title": "SubscriberClient", + "type": "pubsub/v1/subscriberclient" + }] }] }, { @@ -143,6 +199,13 @@ "nav": [{ "title": "Operation", "type": "speech/operation" + }, { + "title": "v1beta1", + "type": "speech/v1beta1/readme", + "nav": [{ + "title": "SpeechClient", + "type": "speech/v1beta1/speechclient" + }] }] }, { diff --git a/src/ErrorReporting/README.md b/src/ErrorReporting/README.md new file mode 100644 index 000000000000..6d2df572ba92 --- /dev/null +++ b/src/ErrorReporting/README.md @@ -0,0 +1,5 @@ +# Stackdriver Error Reporting + +Stackdriver Error Reporting counts, analyzes and aggregates the crashes in your running cloud services. + +For more information, see [cloud.google.com](https://cloud.google.com/error-reporting/). diff --git a/src/ErrorReporting/V1beta1/README.md b/src/ErrorReporting/V1beta1/README.md new file mode 100644 index 000000000000..6d2df572ba92 --- /dev/null +++ b/src/ErrorReporting/V1beta1/README.md @@ -0,0 +1,5 @@ +# Stackdriver Error Reporting + +Stackdriver Error Reporting counts, analyzes and aggregates the crashes in your running cloud services. + +For more information, see [cloud.google.com](https://cloud.google.com/error-reporting/). diff --git a/src/Logging/V2/README.md b/src/Logging/V2/README.md new file mode 100644 index 000000000000..b4bfdb47a63a --- /dev/null +++ b/src/Logging/V2/README.md @@ -0,0 +1,5 @@ +# Stackdriver Logging + +Stackdriver Logging allows you to store, search, analyze, monitor, and alert on log data and events from Google Cloud Platform and Amazon Web Services (AWS). + +For more information, see [cloud.google.com](https://cloud.google.com/logging/). diff --git a/src/Monitoring/README.md b/src/Monitoring/README.md new file mode 100644 index 000000000000..b6936242f5b0 --- /dev/null +++ b/src/Monitoring/README.md @@ -0,0 +1,5 @@ +# Stackdriver Monitoring + +Stackdriver Monitoring provides visibility into the performance, uptime, and overall health of cloud-powered applications. + +For more information, see [cloud.google.com](https://cloud.google.com/monitoring/). diff --git a/src/Monitoring/V3/README.md b/src/Monitoring/V3/README.md new file mode 100644 index 000000000000..b6936242f5b0 --- /dev/null +++ b/src/Monitoring/V3/README.md @@ -0,0 +1,5 @@ +# Stackdriver Monitoring + +Stackdriver Monitoring provides visibility into the performance, uptime, and overall health of cloud-powered applications. + +For more information, see [cloud.google.com](https://cloud.google.com/monitoring/). diff --git a/src/PubSub/V1/README.md b/src/PubSub/V1/README.md new file mode 100644 index 000000000000..ff9c65ee047f --- /dev/null +++ b/src/PubSub/V1/README.md @@ -0,0 +1,5 @@ +# Cloud Pub\Sub + +Cloud Pub/Sub is a fully-managed real-time messaging service that allows you to send and receive messages between independent applications. + +For more information, see [cloud.google.com](https://cloud.google.com/pubsub/). diff --git a/src/Speech/V1beta1/README.md b/src/Speech/V1beta1/README.md new file mode 100644 index 000000000000..9d2f7afd62a2 --- /dev/null +++ b/src/Speech/V1beta1/README.md @@ -0,0 +1,5 @@ +# Cloud Speech + +Google Cloud Speech API enables developers to convert audio to text by applying powerful neural network models in an easy to use API. + +For more information, see [cloud.google.com](https://cloud.google.com/speech/). From 3128001d5f00ed9c19aae3252bdcfac1ae605f1b Mon Sep 17 00:00:00 2001 From: michaelbausor Date: Tue, 7 Mar 2017 15:55:26 -0800 Subject: [PATCH 089/107] Update README for Vision going beta (#387) --- README.md | 66 +++++++++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 862252da3174..7c977e536c9e 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ This client supports the following Google Cloud Platform services at a [Beta](#v * [Google Stackdriver Logging](#google-stackdriver-logging-beta) (Beta) * [Google Cloud Datastore](#google-cloud-datastore-beta) (Beta) * [Google Cloud Storage](#google-cloud-storage-beta) (Beta) +* [Google Cloud Vision](#google-cloud-vision-beta) (Beta) This client supports the following Google Cloud Platform services at an [Alpha](#versioning) quality level: * [Google Cloud Natural Language](#google-cloud-natural-language-alpha) (Alpha) * [Google Cloud Pub/Sub](#google-cloud-pubsub-alpha) (Alpha) * [Google Cloud Speech](#google-cloud-speech-alpha) (Alpha) * [Google Cloud Translation](#google-cloud-translation-alpha) (Alpha) -* [Google Cloud Vision](#google-cloud-vision-alpha) (Alpha) If you need support for other Google APIs, please check out the [Google APIs Client Library for PHP](https://github.com/google/google-api-php-client). @@ -167,6 +167,38 @@ $storage->registerStreamWrapper(); $contents = file_get_contents('gs://my_bucket/file_backup.txt'); ``` +## Google Cloud Vision (Beta) + +- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/vision/visionclient) +- [Official Documentation](https://cloud.google.com/vision/docs) + +#### Preview + +```php +require 'vendor/autoload.php'; + +use Google\Cloud\Vision\VisionClient; + +$vision = new VisionClient([ + 'projectId' => 'my_project' +]); + +// Annotate an image, detecting faces. +$image = $vision->image( + fopen('/data/family_photo.jpg', 'r'), + ['faces'] +); + +$annotation = $vision->annotate($image); + +// Determine if the detected faces have headwear. +foreach ($annotation->faces() as $key => $face) { + if ($face->hasHeadwear()) { + echo "Face $key has headwear.\n"; + } +} +``` + ## Google Cloud Translation (Alpha) - [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/translate/translateclient) @@ -318,38 +350,6 @@ foreach ($results as $result) { } ``` -## Google Cloud Vision (Alpha) - -- [API Documentation](http://googlecloudplatform.github.io/google-cloud-php/#/docs/latest/vision/visionclient) -- [Official Documentation](https://cloud.google.com/vision/docs) - -#### Preview - -```php -require 'vendor/autoload.php'; - -use Google\Cloud\Vision\VisionClient; - -$vision = new VisionClient([ - 'projectId' => 'my_project' -]); - -// Annotate an image, detecting faces. -$image = $vision->image( - fopen('/data/family_photo.jpg', 'r'), - ['faces'] -); - -$annotation = $vision->annotate($image); - -// Determine if the detected faces have headwear. -foreach ($annotation->faces() as $key => $face) { - if ($face->hasHeadwear()) { - echo "Face $key has headwear.\n"; - } -} -``` - ## Caching Access Tokens By default the library will use a simple in-memory caching implementation, however it is possible to override this behavior by passing a [PSR-6](http://www.php-fig.org/psr/psr-6/) caching implementation in to the desired client. From 9b3866a0ad2b7b623ab334a5e231f5d3586ce901 Mon Sep 17 00:00:00 2001 From: David Supplee Date: Wed, 8 Mar 2017 16:33:27 -0500 Subject: [PATCH 090/107] Add documentation about the expected behavior of encoding type (#382) * add documentation about the expected behavior of the encoding type param * note utf-8 requirement when providing a string * wrap json_encode/json_decode * fix broken test --- src/ClientTrait.php | 9 ++- src/JsonTrait.php | 68 ++++++++++++++++++ src/Logger/AppEngineFlexFormatter.php | 5 +- src/NaturalLanguage/NaturalLanguageClient.php | 48 ++++++++++--- src/RequestBuilder.php | 6 +- src/RequestWrapper.php | 9 ++- src/RestTrait.php | 5 +- src/Upload/MultipartUploader.php | 7 +- src/Upload/ResumableUploader.php | 9 ++- tests/unit/JsonTraitTest.php | 69 +++++++++++++++++++ tests/unit/RequestWrapperTest.php | 5 +- 11 files changed, 216 insertions(+), 24 deletions(-) create mode 100644 src/JsonTrait.php create mode 100644 tests/unit/JsonTraitTest.php diff --git a/src/ClientTrait.php b/src/ClientTrait.php index e2f9e63b5256..251016c593fd 100644 --- a/src/ClientTrait.php +++ b/src/ClientTrait.php @@ -23,6 +23,7 @@ use Google\Auth\HttpHandler\HttpHandlerFactory; use Google\Cloud\Compute\Metadata; use Google\Cloud\Exception\GoogleException; +use Google\Cloud\JsonTrait; use GuzzleHttp\Psr7; /** @@ -30,6 +31,8 @@ */ trait ClientTrait { + use JsonTrait; + /** * @var string The project ID created in the Google Developers Console. */ @@ -114,9 +117,9 @@ private function getKeyFile(array $config = []) throw new GoogleException('Given keyfile path does not exist'); } - $keyFileData = json_decode(file_get_contents($config['keyFilePath']), true); - - if (json_last_error() !== JSON_ERROR_NONE) { + try { + $keyFileData = $this->jsonDecode(file_get_contents($config['keyFilePath']), true); + } catch (\InvalidArgumentException $ex) { throw new GoogleException('Given keyfile was invalid'); } diff --git a/src/JsonTrait.php b/src/JsonTrait.php new file mode 100644 index 000000000000..ef161b6fa394 --- /dev/null +++ b/src/JsonTrait.php @@ -0,0 +1,68 @@ +jsonEncode($payload); } } diff --git a/src/NaturalLanguage/NaturalLanguageClient.php b/src/NaturalLanguage/NaturalLanguageClient.php index 3861c096a02a..2b3001e52b69 100644 --- a/src/NaturalLanguage/NaturalLanguageClient.php +++ b/src/NaturalLanguage/NaturalLanguageClient.php @@ -120,7 +120,8 @@ public function __construct(array $config = []) * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeEntities Analyze Entities API documentation * @codingStandardsIgnoreEnd * - * @param string|StorageObject $content The content to analyze. + * @param string|StorageObject $content The content to analyze. When + * providing a string it should be UTF-8 encoded. * @param array $options [optional] { * Configuration options. * @@ -132,7 +133,14 @@ public function __construct(array $config = []) * detected by the service. * @type string $encodingType The text encoding type used by the API to * calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`, - * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. + * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. Please note + * the following behaviors for the encoding type setting: `"NONE"` + * will return a value of "-1" for offsets. `"UTF8"` will + * return byte offsets. `"UTF16"` will return + * [code unit](http://unicode.org/glossary/#code_unit) offsets. + * `"UTF32"` will return + * [unicode character](http://unicode.org/glossary/#character) + * offsets. * } * @return Annotation */ @@ -162,7 +170,8 @@ public function analyzeEntities($content, array $options = []) * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeSentiment Analyze Sentiment API documentation * @codingStandardsIgnoreEnd * - * @param string|StorageObject $content The content to analyze. + * @param string|StorageObject $content The content to analyze. When + * providing a string it should be UTF-8 encoded. * @param array $options [optional] { * Configuration options. * @@ -174,7 +183,14 @@ public function analyzeEntities($content, array $options = []) * detected by the service. * @type string $encodingType The text encoding type used by the API to * calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`, - * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. + * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. Please note + * the following behaviors for the encoding type setting: `"NONE"` + * will return a value of "-1" for offsets. `"UTF8"` will + * return byte offsets. `"UTF16"` will return + * [code unit](http://unicode.org/glossary/#code_unit) offsets. + * `"UTF32"` will return + * [unicode character](http://unicode.org/glossary/#character) + * offsets. * } * @return Annotation */ @@ -203,7 +219,8 @@ public function analyzeSentiment($content, array $options = []) * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/analyzeSyntax Analyze Syntax API documentation * @codingStandardsIgnoreEnd * - * @param string|StorageObject $content The content to analyze. + * @param string|StorageObject $content The content to analyze. When + * providing a string it should be UTF-8 encoded. * @param array $options [optional] { * Configuration options. * @@ -215,7 +232,14 @@ public function analyzeSentiment($content, array $options = []) * detected by the service. * @type string $encodingType The text encoding type used by the API to * calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`, - * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. + * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. Please note + * the following behaviors for the encoding type setting: `"NONE"` + * will return a value of "-1" for offsets. `"UTF8"` will + * return byte offsets. `"UTF16"` will return + * [code unit](http://unicode.org/glossary/#code_unit) offsets. + * `"UTF32"` will return + * [unicode character](http://unicode.org/glossary/#character) + * offsets. * } * @return Annotation */ @@ -256,7 +280,8 @@ public function analyzeSyntax($content, array $options = []) * @see https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/documents/annotateText Annotate Text API documentation * @codingStandardsIgnoreEnd * - * @param string|StorageObject $content The content to annotate. + * @param string|StorageObject $content The content to analyze. When + * providing a string it should be UTF-8 encoded. * @param array $options [optional] { * Configuration options. * @@ -271,7 +296,14 @@ public function analyzeSyntax($content, array $options = []) * detected by the service. * @type string $encodingType The text encoding type used by the API to * calculate offsets. Acceptable values are `"NONE"`, `"UTF8"`, - * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. + * `"UTF16"` and `"UTF32"`. **Defaults to** `"UTF8"`. Please note + * the following behaviors for the encoding type setting: `"NONE"` + * will return a value of "-1" for offsets. `"UTF8"` will + * return byte offsets. `"UTF16"` will return + * [code unit](http://unicode.org/glossary/#code_unit) offsets. + * `"UTF32"` will return + * [unicode character](http://unicode.org/glossary/#character) + * offsets. * } * @return Annotation */ diff --git a/src/RequestBuilder.php b/src/RequestBuilder.php index f0b8064f1e0b..e15928d6ccd6 100644 --- a/src/RequestBuilder.php +++ b/src/RequestBuilder.php @@ -17,6 +17,7 @@ namespace Google\Cloud; +use Google\Cloud\JsonTrait; use Google\Cloud\UriTrait; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Request; @@ -27,6 +28,7 @@ */ class RequestBuilder { + use JsonTrait; use UriTrait; /** @@ -126,7 +128,7 @@ public function build($resource, $method, array $options = []) $action['httpMethod'], $uri, ['Content-Type' => 'application/json'], - $body ? json_encode($body) : null + $body ? $this->jsonEncode($body) : null ); } @@ -136,7 +138,7 @@ public function build($resource, $method, array $options = []) */ private function loadServiceDefinition($servicePath) { - return json_decode( + return $this->jsonDecode( file_get_contents($servicePath, true), true ); diff --git a/src/RequestWrapper.php b/src/RequestWrapper.php index 7dbf528c5e31..979118288f20 100644 --- a/src/RequestWrapper.php +++ b/src/RequestWrapper.php @@ -20,6 +20,7 @@ use Google\Auth\FetchAuthTokenInterface; use Google\Auth\HttpHandler\HttpHandlerFactory; use Google\Cloud\Exception; +use Google\Cloud\JsonTrait; use Google\Cloud\RequestWrapperTrait; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7; @@ -32,6 +33,7 @@ */ class RequestWrapper { + use JsonTrait; use RequestWrapperTrait; /** @@ -231,9 +233,12 @@ private function getExceptionMessage(\Exception $ex) { if ($ex instanceof RequestException && $ex->hasResponse()) { $res = (string) $ex->getResponse()->getBody(); - json_decode($res); - if (json_last_error() === JSON_ERROR_NONE) { + + try { + $this->jsonDecode($res); return $res; + } catch (\InvalidArgumentException $e) { + // no-op } } diff --git a/src/RestTrait.php b/src/RestTrait.php index 8a451d0ca395..f7b493333d39 100644 --- a/src/RestTrait.php +++ b/src/RestTrait.php @@ -17,6 +17,7 @@ namespace Google\Cloud; +use Google\Cloud\JsonTrait; use Google\Cloud\RequestBuilder; use Google\Cloud\RequestWrapper; @@ -25,6 +26,8 @@ */ trait RestTrait { + use JsonTrait; + /** * @var RequestBuilder Builds PSR7 requests from a service definition. */ @@ -73,7 +76,7 @@ public function send($resource, $method, array $options = []) 'retries' => null ]); - return json_decode( + return $this->jsonDecode( $this->requestWrapper->send( $this->requestBuilder->build($resource, $method, $options), $requestOptions diff --git a/src/Upload/MultipartUploader.php b/src/Upload/MultipartUploader.php index 90b01471bcc0..0b77e5ab9338 100644 --- a/src/Upload/MultipartUploader.php +++ b/src/Upload/MultipartUploader.php @@ -17,6 +17,7 @@ namespace Google\Cloud\Upload; +use Google\Cloud\JsonTrait; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\Request; @@ -25,6 +26,8 @@ */ class MultipartUploader extends AbstractUploader { + use JsonTrait; + /** * Triggers the upload process. * @@ -36,7 +39,7 @@ public function upload() [ 'name' => 'metadata', 'headers' => ['Content-Type' => 'application/json; charset=UTF-8'], - 'contents' => json_encode($this->metadata) + 'contents' => $this->jsonEncode($this->metadata) ], [ 'name' => 'data', @@ -50,7 +53,7 @@ public function upload() 'Content-Length' => $multipartStream->getSize() ]; - return json_decode( + return $this->jsonDecode( $this->requestWrapper->send( new Request( 'POST', diff --git a/src/Upload/ResumableUploader.php b/src/Upload/ResumableUploader.php index 69666282c233..787ede57c647 100644 --- a/src/Upload/ResumableUploader.php +++ b/src/Upload/ResumableUploader.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Upload; use Google\Cloud\Exception\GoogleException; +use Google\Cloud\JsonTrait; use GuzzleHttp\Psr7; use GuzzleHttp\Psr7\LimitStream; use GuzzleHttp\Psr7\Request; @@ -28,6 +29,8 @@ */ class ResumableUploader extends AbstractUploader { + use JsonTrait; + /** * @var int */ @@ -69,7 +72,7 @@ public function resume($resumeUri) $response = $this->getStatusResponse(); if ($response->getBody()->getSize() > 0) { - return json_decode($response->getBody(), true); + return $this->jsonDecode($response->getBody(), true); } $this->rangeStart = $this->getRangeStart($response->getHeaderLine('Range')); @@ -122,7 +125,7 @@ public function upload() $rangeStart = $this->getRangeStart($response->getHeaderLine('Range')); } while ($response->getStatusCode() === 308); - return json_decode($response->getBody(), true); + return $this->jsonDecode($response->getBody(), true); } /** @@ -142,7 +145,7 @@ private function createResumeUri() 'POST', $this->uri, $headers, - json_encode($this->metadata) + $this->jsonEncode($this->metadata) ); $response = $this->requestWrapper->send($request, $this->requestOptions); diff --git a/tests/unit/JsonTraitTest.php b/tests/unit/JsonTraitTest.php new file mode 100644 index 000000000000..b4d94b171aa6 --- /dev/null +++ b/tests/unit/JsonTraitTest.php @@ -0,0 +1,69 @@ +implementation = new JsonTraitStub(); + } + + public function testJsonEncode() + { + $this->assertEquals('10', $this->implementation->call('jsonEncode', [10])); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testJsonEncodeThrowsException() + { + $this->implementation->call('jsonEncode', [fopen('php://temp', 'r')]); + } + + public function testJsonDecode() + { + $this->assertEquals(10, $this->implementation->call('jsonDecode', ['10'])); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testJsonDecodeThrowsException() + { + $this->implementation->call('jsonDecode', ['.|.']); + } +} + +class JsonTraitStub +{ + use JsonTrait; + + public function call($fn, array $args) + { + return call_user_func_array([$this, $fn], $args); + } +} diff --git a/tests/unit/RequestWrapperTest.php b/tests/unit/RequestWrapperTest.php index a1c453431b60..3158836dd5b5 100644 --- a/tests/unit/RequestWrapperTest.php +++ b/tests/unit/RequestWrapperTest.php @@ -238,11 +238,12 @@ public function testExceptionMessageIsNotTruncatedWithGuzzle() $requestWrapper = new RequestWrapper([ 'httpHandler' => function ($request, $options = []) { $msg = str_repeat('0', 121); + $jsonMsg = '{"msg":"' . $msg . '"}'; throw new RequestException( - $msg, + $jsonMsg, $request, - new Response(400, [], $msg) + new Response(400, [], $jsonMsg) ); } ]); From b73a782e251858fefa42cce0784377e48d21db2a Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Wed, 8 Mar 2017 16:48:09 -0500 Subject: [PATCH 091/107] Prepare v0.23.0 (#389) --- docs/manifest.json | 1 + src/ServiceBuilder.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/manifest.json b/docs/manifest.json index 718437aa6dd4..5fc7b1af8f4d 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -7,6 +7,7 @@ "matchPartialServiceId": true, "markdown": "php", "versions": [ + "v0.23.0", "v0.22.0", "v0.21.1", "v0.21.0", diff --git a/src/ServiceBuilder.php b/src/ServiceBuilder.php index d6eada64d808..c8d2bfaf7419 100644 --- a/src/ServiceBuilder.php +++ b/src/ServiceBuilder.php @@ -48,7 +48,7 @@ */ class ServiceBuilder { - const VERSION = '0.22.0'; + const VERSION = '0.23.0'; /** * @var array Configuration options to be used between clients. From 3e21b68005e89205ffc1cda9c46fc1fb879ee056 Mon Sep 17 00:00:00 2001 From: John Pedrie Date: Fri, 10 Mar 2017 17:24:36 -0500 Subject: [PATCH 092/107] Component split (#360) * Reorganize namespaces and fix tests * # This is a combination of 9 commits. # This is the 1st commit message: Reorganize namespaces and fix tests Update component versions via the CLI # This is the commit message #2: Create tags in components # This is the commit message #3: Send component name and version in user agent # This is the commit message #4: Add README files # This is the commit message #5: Fix CLI # This is the commit message #6: Add split to travis # This is the commit message #7: Test datastore release # This is the commit message #8: Compile splitsh # This is the commit message #9: Reintroduce full split process * # This is a combination of 2 commits. # This is the 1st commit message: Reorganize namespaces and fix tests Update component versions via the CLI Create tags in components Send component name and version in user agent Add README files Fix CLI Add split to travis Test datastore release Compile splitsh Reintroduce full split process Release patches # This is the commit message #2: Run split in each build matrix * Reorganize namespaces and fix tests Update component versions via the CLI Create tags in components Send component name and version in user agent Add README files Fix CLI Add split to travis Test datastore release Compile splitsh Reintroduce full split process Release patches Run split in each build matrix Release datastore patch enable set -e * Work with github API directly * Fix Vision class reference * Create VERSION file for all components * Remove componentName * Fix int64 snippet tests * Update component release target * Parse documentation separately for each component * This works now * Add flag to docs to use JSON_PRETTY_PRINT for debugging * Parse correct links between components * Fix unit tests * Doc links within component should not change version * Cross-component links should go to the correct version docs * Update snippets to remove refs to ServiceBuilder * Remove unnecessary env vars * Don't write VERSION for parent * Release packages * release v0.24.1 * Add autoloaders to each component * Pass libVersion to gax config * Update push docs command * add gcsUri helper * allow gcsUri or storage object * Release 0.24.2 * Update things again * this is the worst * Link to upstream tag * tag * Update table of contents * Reset versions * Update suggests and requires * fix build docs trigger * dont throw exceptions on decode * Fix docs link * Fix various doc issues * fix namespace --- .travis.yml | 35 +-- composer.json | 8 + dev/google-cloud | 2 + dev/sh/build-docs | 8 +- dev/sh/compile-splitsh | 13 + dev/sh/push-docs | 19 +- dev/sh/split | 10 + dev/sh/trigger-split | 9 + dev/src/DocGenerator/Command/Docs.php | 100 +++++++- dev/src/DocGenerator/DocGenerator.php | 56 +++- dev/src/DocGenerator/Parser/CodeParser.php | 90 ++++++- .../DocGenerator/Parser/MarkdownParser.php | 2 +- dev/src/DocGenerator/TableOfContents.php | 44 ++++ dev/src/DocGenerator/TypeGenerator.php | 4 +- dev/src/DocGenerator/Writer.php | 12 +- dev/src/GetComponentsTrait.php | 116 +++++++++ dev/src/Release/Command/Release.php | 131 +++++++--- dev/src/Snippet/Parser/Snippet.php | 11 +- dev/src/Split/Command/Split.php | 172 +++++++++++++ docs/contents/cloud-bigquery.json | 31 +++ docs/contents/cloud-core.json | 30 +++ docs/contents/cloud-datastore.json | 25 ++ docs/contents/cloud-error-reporting.json | 20 ++ docs/contents/cloud-logging.json | 35 +++ docs/contents/cloud-monitoring.json | 17 ++ docs/contents/cloud-natural-language.json | 7 + docs/contents/cloud-pubsub.json | 26 ++ docs/contents/cloud-speech.json | 17 ++ docs/contents/cloud-storage.json | 13 + docs/contents/cloud-translate.json | 4 + docs/contents/cloud-vision.json | 34 +++ docs/contents/google-cloud.json | 4 + docs/home.html | 2 +- docs/manifest.json | 162 +++++++++--- docs/toc.json | 239 +----------------- src/BigQuery/BigQueryClient.php | 21 +- src/BigQuery/Bytes.php | 5 +- .../Connection/ConnectionInterface.php | 2 + src/BigQuery/Connection/Rest.php | 18 +- src/BigQuery/Dataset.php | 2 +- src/BigQuery/Date.php | 5 +- src/BigQuery/Job.php | 2 +- src/BigQuery/LICENSE | 202 +++++++++++++++ src/BigQuery/QueryResults.php | 4 +- src/BigQuery/README.md | 10 + src/BigQuery/Table.php | 17 +- src/BigQuery/Time.php | 4 + src/BigQuery/Timestamp.php | 4 + src/BigQuery/ValueMapper.php | 8 +- src/BigQuery/composer.json | 26 ++ src/{ => Core}/ArrayTrait.php | 2 +- src/{ => Core}/CallTrait.php | 2 +- src/{ => Core}/ClientTrait.php | 7 +- src/{ => Core}/Compute/Metadata.php | 8 +- .../Metadata/Readers/ReaderInterface.php | 2 +- .../Compute/Metadata/Readers/StreamReader.php | 2 +- src/{ => Core}/EmulatorTrait.php | 6 +- .../Exception/BadRequestException.php | 2 +- .../Exception/ConflictException.php | 2 +- src/{ => Core}/Exception/GoogleException.php | 2 +- .../Exception/NotFoundException.php | 2 +- src/{ => Core}/Exception/ServerException.php | 2 +- src/{ => Core}/Exception/ServiceException.php | 2 +- src/{ => Core}/ExponentialBackoff.php | 2 +- src/{ => Core}/GrpcRequestWrapper.php | 12 +- src/{ => Core}/GrpcTrait.php | 11 +- src/{ => Core}/Iam/Iam.php | 14 +- src/{ => Core}/Iam/IamConnectionInterface.php | 2 +- src/{ => Core}/Iam/PolicyBuilder.php | 4 +- src/{ => Core}/Int64.php | 2 +- src/{ => Core}/JsonTrait.php | 2 +- src/Core/LICENSE | 202 +++++++++++++++ .../Logger/AppEngineFlexFormatter.php | 4 +- .../Logger/AppEngineFlexHandler.php | 2 +- src/{ => Core}/PhpArray.php | 2 +- src/Core/README.md | 7 + src/{ => Core}/RequestBuilder.php | 4 +- src/{ => Core}/RequestWrapper.php | 27 +- src/{ => Core}/RequestWrapperTrait.php | 2 +- src/{ => Core}/RestTrait.php | 8 +- src/{ => Core}/Upload/AbstractUploader.php | 6 +- src/{ => Core}/Upload/MultipartUploader.php | 4 +- src/{ => Core}/Upload/ResumableUploader.php | 6 +- src/{ => Core}/Upload/StreamableUploader.php | 2 +- src/{ => Core}/UriTrait.php | 2 +- src/{ => Core}/ValidateTrait.php | 2 +- src/Core/composer.json | 28 ++ src/Datastore/Blob.php | 5 +- src/Datastore/Connection/Rest.php | 14 +- src/Datastore/DatastoreClient.php | 29 +-- src/Datastore/DatastoreSessionHandler.php | 12 +- src/Datastore/Entity.php | 7 +- src/Datastore/EntityMapper.php | 6 +- src/Datastore/GeoPoint.php | 6 +- src/Datastore/Key.php | 7 +- src/Datastore/LICENSE | 202 +++++++++++++++ src/Datastore/Operation.php | 2 +- src/Datastore/Query/GqlQuery.php | 7 +- src/Datastore/Query/Query.php | 5 +- src/Datastore/README.md | 10 + src/Datastore/Transaction.php | 5 +- src/Datastore/composer.json | 22 ++ src/ErrorReporting/README.md | 3 + src/ErrorReporting/composer.json | 24 ++ src/Logging/Connection/Grpc.php | 9 +- src/Logging/Connection/Rest.php | 12 +- src/Logging/Entry.php | 5 +- src/Logging/LICENSE | 202 +++++++++++++++ src/Logging/Logger.php | 9 +- src/Logging/LoggingClient.php | 12 +- src/Logging/Metric.php | 7 +- src/Logging/PsrLogger.php | 5 +- src/Logging/README.md | 10 + src/Logging/Sink.php | 7 +- src/Logging/composer.json | 26 ++ src/Monitoring/LICENSE | 202 +++++++++++++++ src/Monitoring/README.md | 3 + src/Monitoring/composer.json | 24 ++ src/NaturalLanguage/Annotation.php | 7 +- src/NaturalLanguage/Connection/Rest.php | 12 +- src/NaturalLanguage/LICENSE | 202 +++++++++++++++ src/NaturalLanguage/NaturalLanguageClient.php | 67 +++-- src/NaturalLanguage/README.md | 10 + src/NaturalLanguage/composer.json | 25 ++ src/PubSub/Connection/Grpc.php | 11 +- src/PubSub/Connection/IamSubscription.php | 2 +- src/PubSub/Connection/IamTopic.php | 2 +- src/PubSub/Connection/Rest.php | 14 +- src/PubSub/IncomingMessageTrait.php | 2 +- src/PubSub/LICENSE | 202 +++++++++++++++ src/PubSub/Message.php | 5 +- src/PubSub/PubSubClient.php | 20 +- src/PubSub/README.md | 139 +--------- src/PubSub/Subscription.php | 13 +- src/PubSub/Topic.php | 10 +- src/PubSub/composer.json | 26 ++ src/ServiceBuilder.php | 4 +- src/Speech/Connection/Rest.php | 12 +- src/Speech/LICENSE | 202 +++++++++++++++ src/Speech/Operation.php | 7 +- src/Speech/README.md | 10 + src/Speech/SpeechClient.php | 42 +-- src/Speech/composer.json | 27 ++ src/Storage/Acl.php | 5 +- src/Storage/Bucket.php | 12 +- src/Storage/Connection/Rest.php | 20 +- src/Storage/LICENSE | 202 +++++++++++++++ src/Storage/README.md | 10 + src/Storage/StorageClient.php | 13 +- src/Storage/StorageObject.php | 27 +- src/Storage/StreamWrapper.php | 4 +- src/Storage/WriteStream.php | 2 +- src/Storage/composer.json | 22 ++ src/Translate/Connection/Rest.php | 12 +- src/Translate/LICENSE | 202 +++++++++++++++ src/Translate/README.md | 10 + src/Translate/TranslateClient.php | 13 +- src/Translate/composer.json | 22 ++ src/Vision/Annotation.php | 5 +- src/Vision/Annotation/CropHint.php | 7 +- src/Vision/Annotation/Document.php | 7 +- src/Vision/Annotation/Entity.php | 7 +- src/Vision/Annotation/Face.php | 7 +- src/Vision/Annotation/Face/Landmarks.php | 5 +- src/Vision/Annotation/ImageProperties.php | 5 +- src/Vision/Annotation/SafeSearch.php | 7 +- src/Vision/Annotation/Web.php | 5 +- src/Vision/Annotation/Web/WebEntity.php | 7 +- src/Vision/Annotation/Web/WebImage.php | 7 +- src/Vision/Annotation/Web/WebPage.php | 7 +- src/Vision/Connection/Rest.php | 12 +- src/Vision/Image.php | 15 +- src/Vision/LICENSE | 202 +++++++++++++++ src/Vision/README.md | 10 + src/Vision/VisionClient.php | 16 +- src/Vision/composer.json | 25 ++ .../snippets/BigQuery/BigQueryClientTest.php | 10 +- tests/snippets/BigQuery/TableTest.php | 2 +- .../{ => Core}/Compute/MetadataTest.php | 6 +- tests/snippets/{ => Core}/Iam/IamTest.php | 6 +- .../{ => Core}/Iam/PolicyBuilderTest.php | 4 +- tests/snippets/{ => Core}/Int64Test.php | 4 +- .../Datastore/DatastoreClientTest.php | 14 +- tests/snippets/Logging/LoggingClientTest.php | 8 - .../NaturalLanguageClientTest.php | 7 - tests/snippets/PubSub/MessageTest.php | 2 +- tests/snippets/PubSub/PubSubClientTest.php | 12 +- tests/snippets/PubSub/SubscriptionTest.php | 2 +- tests/snippets/PubSub/TopicTest.php | 2 +- tests/snippets/Speech/SpeechClientTest.php | 8 - tests/snippets/Storage/BucketTest.php | 8 +- tests/snippets/Storage/StorageClientTest.php | 7 - tests/snippets/Storage/StorageObjectTest.php | 10 + .../Translate/TranslateClientTest.php | 8 - tests/snippets/Vision/VisionClientTest.php | 8 - tests/snippets/bootstrap.php | 4 +- tests/system/BigQuery/BigQueryTestCase.php | 2 +- .../system/BigQuery/LoadDataAndQueryTest.php | 2 +- tests/system/BigQuery/ManageTablesTest.php | 2 +- tests/system/Datastore/DatastoreTestCase.php | 2 +- tests/system/Datastore/SaveAndModifyTest.php | 2 +- tests/system/Logging/LoggingTestCase.php | 2 +- .../system/Logging/WriteAndListEntryTest.php | 2 +- .../NaturalLanguageTestCase.php | 2 +- tests/system/PubSub/PubSubTestCase.php | 2 +- tests/system/Storage/ManageAclTest.php | 2 +- tests/system/Storage/StorageTestCase.php | 2 +- tests/unit/BigQuery/BigQueryClientTest.php | 2 +- tests/unit/BigQuery/BytesTest.php | 2 +- tests/unit/BigQuery/Connection/RestTest.php | 15 +- tests/unit/BigQuery/DatasetTest.php | 4 +- tests/unit/BigQuery/DateTest.php | 2 +- tests/unit/BigQuery/InsertResponseTest.php | 2 +- tests/unit/BigQuery/JobTest.php | 4 +- tests/unit/BigQuery/QueryResultsTest.php | 4 +- tests/unit/BigQuery/TableTest.php | 26 +- tests/unit/BigQuery/TimeTest.php | 2 +- tests/unit/BigQuery/TimestampTest.php | 2 +- tests/unit/BigQuery/ValueMapperTest.php | 4 +- tests/unit/{ => Core}/ArrayTraitTest.php | 6 +- tests/unit/{ => Core}/CallTraitTest.php | 6 +- tests/unit/{ => Core}/ClientTraitTest.php | 26 +- .../unit/{ => Core}/Compute/MetadataTest.php | 8 +- tests/unit/{ => Core}/EmulatorTraitTest.php | 6 +- .../Exception/ServiceExceptionTest.php | 5 +- .../{ => Core}/ExponentialBackoffTest.php | 6 +- .../{ => Core}/GrpcRequestWrapperTest.php | 21 +- tests/unit/{ => Core}/GrpcTraitTest.php | 15 +- tests/unit/{ => Core}/Iam/IamTest.php | 7 +- .../unit/{ => Core}/Iam/PolicyBuilderTest.php | 5 +- tests/unit/{ => Core}/Int64Test.php | 6 +- tests/unit/{ => Core}/JsonTraitTest.php | 4 +- .../Logger/AppEngineFlexHandlerTest.php | 5 +- tests/unit/{ => Core}/PhpArrayTest.php | 8 +- tests/unit/{ => Core}/RequestBuilderTest.php | 12 +- tests/unit/{ => Core}/RequestWrapperTest.php | 46 ++-- tests/unit/{ => Core}/RestTraitTest.php | 12 +- tests/unit/{ => Core}/ServiceBuilderTest.php | 4 +- .../Upload/MultipartUploaderTest.php | 11 +- .../Upload/ResumableUploaderTest.php | 27 +- .../Upload/StreamableUploaderTest.php | 17 +- tests/unit/{ => Core}/UriTraitTest.php | 6 +- tests/unit/{ => Core}/ValidateTraitTest.php | 6 +- tests/unit/Datastore/BlobTest.php | 2 +- tests/unit/Datastore/Connection/RestTest.php | 7 +- tests/unit/Datastore/DatastoreClientTest.php | 2 +- .../Datastore/DatastoreSessionHandlerTest.php | 2 +- tests/unit/Datastore/DatastoreTraitTest.php | 2 +- tests/unit/Datastore/EntityMapperTest.php | 4 +- tests/unit/Datastore/EntityTest.php | 2 +- tests/unit/Datastore/GeoPointTest.php | 2 +- tests/unit/Datastore/KeyTest.php | 2 +- tests/unit/Datastore/OperationTest.php | 2 +- tests/unit/Datastore/Query/GqlQueryTest.php | 2 +- tests/unit/Datastore/Query/QueryTest.php | 2 +- tests/unit/Datastore/TransactionTest.php | 2 +- tests/unit/JsonFileTest.php | 37 ++- tests/unit/Logging/Connection/GrpcTest.php | 6 +- tests/unit/Logging/Connection/RestTest.php | 7 +- tests/unit/Logging/LoggerTest.php | 2 +- tests/unit/Logging/LoggingClientTest.php | 2 +- tests/unit/Logging/MetricTest.php | 4 +- .../Logging/PsrLoggerCompatabilityTest.php | 2 +- tests/unit/Logging/PsrLoggerTest.php | 2 +- tests/unit/Logging/SinkTest.php | 4 +- tests/unit/NaturalLanguage/AnnotationTest.php | 4 +- .../NaturalLanguage/Connection/RestTest.php | 9 +- .../NaturalLanguageClientTest.php | 20 +- tests/unit/PubSub/Connection/GrpcTest.php | 4 +- .../PubSub/Connection/IamSubscriptionTest.php | 2 +- tests/unit/PubSub/Connection/IamTopicTest.php | 2 +- tests/unit/PubSub/Connection/RestTest.php | 7 +- .../unit/PubSub/IncomingMessageTraitTest.php | 4 +- tests/unit/PubSub/MessageTest.php | 2 +- tests/unit/PubSub/PubSubClientTest.php | 2 +- tests/unit/PubSub/ResourceNameTraitTest.php | 2 +- tests/unit/PubSub/SubscriptionTest.php | 6 +- tests/unit/PubSub/TopicTest.php | 6 +- tests/unit/Speech/Connection/RestTest.php | 7 +- tests/unit/Speech/OperationTest.php | 4 +- tests/unit/Speech/SpeechClientTest.php | 23 +- tests/unit/Storage/AclTest.php | 2 +- tests/unit/Storage/BucketTest.php | 25 +- tests/unit/Storage/Connection/RestTest.php | 23 +- tests/unit/Storage/EncryptionTraitTest.php | 2 +- tests/unit/Storage/StorageClientTest.php | 2 +- tests/unit/Storage/StorageObjectTest.php | 12 +- tests/unit/Storage/StreamWrapperTest.php | 16 +- tests/unit/Storage/WriteStreamTest.php | 2 +- tests/unit/Translate/Connection/RestTest.php | 7 +- tests/unit/Translate/TranslateClientTest.php | 2 +- tests/unit/Vision/Annotation/EntityTest.php | 2 +- .../Vision/Annotation/Face/LandmarksTest.php | 2 +- tests/unit/Vision/Annotation/FaceTest.php | 2 +- .../Vision/Annotation/LikelihoodTraitTest.php | 2 +- .../unit/Vision/Annotation/SafeSearchTest.php | 2 +- tests/unit/Vision/AnnotationTest.php | 2 +- tests/unit/Vision/Connection/RestTest.php | 7 +- tests/unit/Vision/ImageTest.php | 2 +- tests/unit/Vision/VisionClientTest.php | 2 +- 300 files changed, 4638 insertions(+), 1319 deletions(-) create mode 100755 dev/sh/compile-splitsh create mode 100755 dev/sh/split create mode 100755 dev/sh/trigger-split create mode 100644 dev/src/DocGenerator/TableOfContents.php create mode 100644 dev/src/GetComponentsTrait.php create mode 100644 dev/src/Split/Command/Split.php create mode 100644 docs/contents/cloud-bigquery.json create mode 100644 docs/contents/cloud-core.json create mode 100644 docs/contents/cloud-datastore.json create mode 100644 docs/contents/cloud-error-reporting.json create mode 100644 docs/contents/cloud-logging.json create mode 100644 docs/contents/cloud-monitoring.json create mode 100644 docs/contents/cloud-natural-language.json create mode 100644 docs/contents/cloud-pubsub.json create mode 100644 docs/contents/cloud-speech.json create mode 100644 docs/contents/cloud-storage.json create mode 100644 docs/contents/cloud-translate.json create mode 100644 docs/contents/cloud-vision.json create mode 100644 docs/contents/google-cloud.json create mode 100644 src/BigQuery/LICENSE create mode 100644 src/BigQuery/README.md create mode 100644 src/BigQuery/composer.json rename src/{ => Core}/ArrayTrait.php (98%) rename src/{ => Core}/CallTrait.php (97%) rename src/{ => Core}/ClientTrait.php (97%) rename src/{ => Core}/Compute/Metadata.php (93%) rename src/{ => Core}/Compute/Metadata/Readers/ReaderInterface.php (93%) rename src/{ => Core}/Compute/Metadata/Readers/StreamReader.php (96%) rename src/{ => Core}/EmulatorTrait.php (93%) rename src/{ => Core}/Exception/BadRequestException.php (95%) rename src/{ => Core}/Exception/ConflictException.php (95%) rename src/{ => Core}/Exception/GoogleException.php (94%) rename src/{ => Core}/Exception/NotFoundException.php (94%) rename src/{ => Core}/Exception/ServerException.php (95%) rename src/{ => Core}/Exception/ServiceException.php (97%) rename src/{ => Core}/ExponentialBackoff.php (99%) rename src/{ => Core}/GrpcRequestWrapper.php (95%) rename src/{ => Core}/GrpcTrait.php (95%) rename src/{ => Core}/Iam/Iam.php (93%) rename src/{ => Core}/Iam/IamConnectionInterface.php (97%) rename src/{ => Core}/Iam/PolicyBuilder.php (98%) rename src/{ => Core}/Int64.php (98%) rename src/{ => Core}/JsonTrait.php (98%) create mode 100644 src/Core/LICENSE rename src/{ => Core}/Logger/AppEngineFlexFormatter.php (96%) rename src/{ => Core}/Logger/AppEngineFlexHandler.php (98%) rename src/{ => Core}/PhpArray.php (99%) create mode 100644 src/Core/README.md rename src/{ => Core}/RequestBuilder.php (98%) rename src/{ => Core}/RequestWrapper.php (91%) rename src/{ => Core}/RequestWrapperTrait.php (99%) rename src/{ => Core}/RestTrait.php (93%) rename src/{ => Core}/Upload/AbstractUploader.php (96%) rename src/{ => Core}/Upload/MultipartUploader.php (96%) rename src/{ => Core}/Upload/ResumableUploader.php (97%) rename src/{ => Core}/Upload/StreamableUploader.php (98%) rename src/{ => Core}/UriTrait.php (98%) rename src/{ => Core}/ValidateTrait.php (98%) create mode 100644 src/Core/composer.json create mode 100644 src/Datastore/LICENSE create mode 100644 src/Datastore/README.md create mode 100644 src/Datastore/composer.json create mode 100644 src/ErrorReporting/composer.json create mode 100644 src/Logging/LICENSE create mode 100644 src/Logging/README.md create mode 100644 src/Logging/composer.json create mode 100644 src/Monitoring/LICENSE create mode 100644 src/Monitoring/composer.json create mode 100644 src/NaturalLanguage/LICENSE create mode 100644 src/NaturalLanguage/README.md create mode 100644 src/NaturalLanguage/composer.json create mode 100644 src/PubSub/LICENSE create mode 100644 src/PubSub/composer.json create mode 100644 src/Speech/LICENSE create mode 100644 src/Speech/README.md create mode 100644 src/Speech/composer.json create mode 100644 src/Storage/LICENSE create mode 100644 src/Storage/README.md create mode 100644 src/Storage/composer.json create mode 100644 src/Translate/LICENSE create mode 100644 src/Translate/README.md create mode 100644 src/Translate/composer.json create mode 100644 src/Vision/LICENSE create mode 100644 src/Vision/README.md create mode 100644 src/Vision/composer.json rename tests/snippets/{ => Core}/Compute/MetadataTest.php (96%) rename tests/snippets/{ => Core}/Iam/IamTest.php (96%) rename tests/snippets/{ => Core}/Iam/PolicyBuilderTest.php (96%) rename tests/snippets/{ => Core}/Int64Test.php (96%) rename tests/unit/{ => Core}/ArrayTraitTest.php (96%) rename tests/unit/{ => Core}/CallTraitTest.php (93%) rename tests/unit/{ => Core}/ClientTraitTest.php (88%) rename tests/unit/{ => Core}/Compute/MetadataTest.php (92%) rename tests/unit/{ => Core}/EmulatorTraitTest.php (93%) rename tests/unit/{ => Core}/Exception/ServiceExceptionTest.php (90%) rename tests/unit/{ => Core}/ExponentialBackoffTest.php (97%) rename tests/unit/{ => Core}/GrpcRequestWrapperTest.php (89%) rename tests/unit/{ => Core}/GrpcTraitTest.php (96%) rename tests/unit/{ => Core}/Iam/IamTest.php (96%) rename tests/unit/{ => Core}/Iam/PolicyBuilderTest.php (97%) rename tests/unit/{ => Core}/Int64Test.php (92%) rename tests/unit/{ => Core}/JsonTraitTest.php (95%) rename tests/unit/{ => Core}/Logger/AppEngineFlexHandlerTest.php (93%) rename tests/unit/{ => Core}/PhpArrayTest.php (96%) rename tests/unit/{ => Core}/RequestBuilderTest.php (91%) rename tests/unit/{ => Core}/RequestWrapperTest.php (88%) rename tests/unit/{ => Core}/RestTraitTest.php (88%) rename tests/unit/{ => Core}/ServiceBuilderTest.php (97%) rename tests/unit/{ => Core}/Upload/MultipartUploaderTest.php (81%) rename tests/unit/{ => Core}/Upload/ResumableUploaderTest.php (84%) rename tests/unit/{ => Core}/Upload/StreamableUploaderTest.php (89%) rename tests/unit/{ => Core}/UriTraitTest.php (95%) rename tests/unit/{ => Core}/ValidateTraitTest.php (95%) diff --git a/.travis.yml b/.travis.yml index fd8876ac630c..0c37a6d81b32 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,30 +1,31 @@ language: php - sudo: required dist: trusty matrix: - include: - - php: 5.5.38 - - php: 5.6.25 - - php: 7.0 - - php: 7.1 - - php: hhvm - group: edge - fast_finish: true + include: + - php: 5.5.38 + - php: 5.6.25 + - php: 7.0 + - php: 7.1 + - php: hhvm + group: edge + fast_finish: true before_script: - - pecl install grpc || echo 'Failed to install grpc' - - composer install + - pecl install grpc || echo 'Failed to install grpc' + - composer install script: - - ./dev/sh/tests - - vendor/bin/phpcs --standard=./phpcs-ruleset.xml - - ./dev/sh/build-docs + - ./dev/sh/tests + - vendor/bin/phpcs --standard=./phpcs-ruleset.xml + - ./dev/sh/build-docs after_success: - - bash <(curl -s https://codecov.io/bash) - - ./dev/sh/push-docs + - bash <(curl -s https://codecov.io/bash) + - ./dev/sh/push-docs + - ./dev/sh/trigger-split + - cat ./build/snippets-uncovered.json after_failure: - - echo "SNIPPET COVERAGE REPORT" && cat ./build/snippets-uncovered.json + - echo "SNIPPET COVERAGE REPORT" && cat ./build/snippets-uncovered.json diff --git a/composer.json b/composer.json index 4402d1145b28..0ed669718bb4 100644 --- a/composer.json +++ b/composer.json @@ -76,5 +76,13 @@ }, "scripts": { "google-cloud": "dev/google-cloud" + }, + "extra": { + "component": { + "id": "google-cloud", + "target": "git@github.com:jdpedrie-gcp/google-cloud-php.git", + "path": "src", + "entry": "ServiceBuilder.php" + } } } diff --git a/dev/google-cloud b/dev/google-cloud index cd0636602a4f..e004b499d438 100755 --- a/dev/google-cloud +++ b/dev/google-cloud @@ -20,6 +20,7 @@ require __DIR__ . '/../vendor/autoload.php'; use Google\Cloud\Dev\DocGenerator\Command\Docs; use Google\Cloud\Dev\Release\Command\Release; +use Google\Cloud\Dev\Split\Command\Split; use Symfony\Component\Console\Application; if (!class_exists(Application::class)) { @@ -33,4 +34,5 @@ if (!class_exists(Application::class)) { $app = new Application; $app->add(new Release(__DIR__)); $app->add(new Docs(__DIR__)); +$app->add(new Split(__DIR__)); $app->run(); diff --git a/dev/sh/build-docs b/dev/sh/build-docs index cc09be67d534..1079b9de9dd6 100755 --- a/dev/sh/build-docs +++ b/dev/sh/build-docs @@ -5,7 +5,13 @@ set -ev function buildDocs () { echo "doc dir before generation:" find docs - composer google-cloud docs + + if [ -z "$TRAVIS_TAG" ]; then + ./dev/google-cloud docs + else + ./dev/google-cloud docs -r ${TRAVIS_TAG} + fi + echo "doc dir after generation:" find docs } diff --git a/dev/sh/compile-splitsh b/dev/sh/compile-splitsh new file mode 100755 index 000000000000..1cef5094d2b0 --- /dev/null +++ b/dev/sh/compile-splitsh @@ -0,0 +1,13 @@ +#!/bin/bash + +mkdir $ +export GOPATH=$TRAVIS_BUILD_DIR/go + +go get -d github.com/libgit2/git2go +cd $GOPATH/src/github.com/libgit2/git2go +git checkout next +git submodule update --init +make install + +go get github.com/splitsh/lite +go build -o $TRAVIS_BUILD_DIR/splitsh-lite github.com/splitsh/lite diff --git a/dev/sh/push-docs b/dev/sh/push-docs index a0ef3d50e770..0ee349729324 100755 --- a/dev/sh/push-docs +++ b/dev/sh/push-docs @@ -2,20 +2,11 @@ set -ev -function generateDocs () { - echo "doc dir before generation:" - find docs - composer google-cloud docs - echo "doc dir after generation:" - find docs -} - function pushDocs () { - git submodule add -q -f -b gh-pages https://${GH_OAUTH_TOKEN}@github.com/${GH_OWNER}/${GH_PROJECT_NAME} ghpages - mkdir -p ghpages/json/${1} - cp -R docs/json/master/* ghpages/json/${1} - cp docs/overview.html ghpages/json/${1} - cp docs/toc.json ghpages/json/${1} + git submodule add -q -f -b gh-pages https://${GH_OAUTH_TOKEN}@github.com/${TRAVIS_REPO_SLUG} ghpages + + rsync -aP docs/json/* ghpages/json/ + cp docs/home.html ghpages/json cp docs/manifest.json ghpages cd ghpages @@ -25,7 +16,7 @@ function pushDocs () { git config user.email "travis@travis-ci.org" git commit -m "Updating docs for ${1}" git status - git push -q https://${GH_OAUTH_TOKEN}@github.com/${GH_OWNER}/${GH_PROJECT_NAME} HEAD:gh-pages + git push -q https://${GH_OAUTH_TOKEN}@github.com/${TRAVIS_REPO_SLUG} HEAD:gh-pages else echo "Nothing to commit." fi diff --git a/dev/sh/split b/dev/sh/split new file mode 100755 index 000000000000..6ff7c259f9ac --- /dev/null +++ b/dev/sh/split @@ -0,0 +1,10 @@ +#!/bin/bash + +set -e + +branch=$(git branch | sed -n -e 's/^\* \(.*\)/\1/p') + +SHA=`$TRAVIS_BUILD_DIR/splitsh-lite --prefix=$1` +git push -q \ + "https://${GH_OAUTH_TOKEN}@github.com/$2" \ + $SHA:master --force diff --git a/dev/sh/trigger-split b/dev/sh/trigger-split new file mode 100755 index 000000000000..5685a6fe239e --- /dev/null +++ b/dev/sh/trigger-split @@ -0,0 +1,9 @@ +#!/bin/bash + +if [[ "$TRAVIS_JOB_NUMBER" == *.1 && -n "$TRAVIS_TAG" ]]; then + $(dirname $0)/compile-splitsh + git fetch --unshallow + composer google-cloud split +else + echo "Split occurs only in a tag run, and in the first matrix build" +fi diff --git a/dev/src/DocGenerator/Command/Docs.php b/dev/src/DocGenerator/Command/Docs.php index 33c522682c84..2ade7f174d00 100644 --- a/dev/src/DocGenerator/Command/Docs.php +++ b/dev/src/DocGenerator/Command/Docs.php @@ -19,7 +19,9 @@ use Google\Cloud\Dev\DocGenerator\DocGenerator; use Google\Cloud\Dev\DocGenerator\GuideGenerator; +use Google\Cloud\Dev\DocGenerator\TableOfContents; use Google\Cloud\Dev\DocGenerator\TypeGenerator; +use Google\Cloud\Dev\GetComponentsTrait; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use RecursiveRegexIterator; @@ -27,11 +29,16 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class Docs extends Command { - const DEFAULT_OUTPUT_DIR = 'docs/json/master'; + use GetComponentsTrait; + + const DEFAULT_OUTPUT_DIR = 'docs/json'; + const TOC_SOURCE_DIR = 'docs/contents'; + const TOC_TEMPLATE = 'docs/toc.json'; const DEFAULT_SOURCE_DIR = 'src'; private $cliBasePath; @@ -47,29 +54,94 @@ protected function configure() { $this->setName('docs') ->setDescription('Generate Documentation') - ->addArgument('source', InputArgument::OPTIONAL, 'The source directory to traverse and parse') - ->addArgument('output', InputArgument::OPTIONAL, 'The directory to output files into'); + ->addOption('release', 'r', InputOption::VALUE_REQUIRED, 'If set, docs will be generated into tag folders' . + ' such as v1.0.0 rather than master.', false) + ->addOption('pretty', 'p', InputOption::VALUE_OPTIONAL, 'If set, json files will be written with pretty'. + ' formatting using PHP\'s JSON_PRETTY_PRINT flag', false); } protected function execute(InputInterface $input, OutputInterface $output) { + $release = ($input->getOption('release') === false && $input->getOption('release') !== 'false') + ? null + : $input->getOption('release'); + + $pretty = ($input->getOption('pretty') === false) ? false : true; + $paths = [ - 'source' => ($input->getArgument('source')) - ? $this->cliBasePath .'/../'. $input->getArgument('source') - : $this->cliBasePath .'/../'. self::DEFAULT_SOURCE_DIR, + 'source' => $this->cliBasePath .'/../'. self::DEFAULT_SOURCE_DIR, + 'output' => $this->cliBasePath .'/../'. self::DEFAULT_OUTPUT_DIR, + 'project' => $this->cliBasePath .'/../', + 'manifest' => $this->cliBasePath .'/../docs/manifest.json', + 'toc' => $this->cliBasePath .'/../'. self::TOC_SOURCE_DIR, + 'tocTemplate' => $this->cliBasePath .'/../'. self::TOC_TEMPLATE + ]; - 'output' => ($input->getArgument('output')) - ? $this->cliBasePath .'/../'. $input->getArgument('output') - : $this->cliBasePath .'/../'. self::DEFAULT_OUTPUT_DIR + $components = $this->getComponents($paths['source']); + $tocTemplate = json_decode(file_get_contents($paths['tocTemplate']), true); + + foreach ($components as $component) { + $input = $paths['project'] . $component['path']; + $source = $this->getFilesList($input); + $this->generateComponentDocumentation($output, $source, $component, $paths, $tocTemplate, $release, $pretty); + } + + $source = [$paths['project'] .'src/ServiceBuilder.php']; + $component = [ + 'id' => 'google-cloud', + 'path' => 'src/' ]; + $this->generateComponentDocumentation($output, $source, $component, $paths, $tocTemplate, $release, $pretty); + } + + private function generateComponentDocumentation( + OutputInterface $output, + array $source, + array $component, + array $paths, + $tocTemplate, + $release = false, + $pretty = false + ) { + $output->writeln(sprintf('Writing documentation for %s', $component['id'])); + $output->writeln('--------------'); + + $version = $this->getComponentVersion($paths['manifest'], $component['id']); + + $outputPath = ($release) + ? $paths['output'] .'/'. $component['id'] .'/'. $version + : $paths['output'] .'/'. $component['id'] .'/master'; + + $output->writeln(sprintf('Writing to %s', realpath($outputPath))); + + $types = new TypeGenerator($outputPath); + + $docs = new DocGenerator( + $types, + $source, + $outputPath, + $this->cliBasePath, + $component['id'], + $paths['manifest'], + $release + ); + $docs->generate($component['path'], $pretty); + + $types->write($pretty); - $types = new TypeGenerator($paths['output']); + $output->writeln(sprintf('Writing table of contents to %s', realpath($outputPath))); + $services = json_decode(file_get_contents($paths['toc'] .'/'. $component['id'] .'.json'), true); - $sourceFiles = $this->getFilesList($paths['source']); - $docs = new DocGenerator($types, $sourceFiles, $paths['output'], $this->cliBasePath); - $docs->generate(); + $toc = new TableOfContents( + $tocTemplate, + $services, + $release, + $outputPath + ); + $toc->generate($pretty); - $types->write(); + $output->writeln(' '); + $output->writeln(' '); } private function getFilesList($source) diff --git a/dev/src/DocGenerator/DocGenerator.php b/dev/src/DocGenerator/DocGenerator.php index 7394002af896..239e54eb1dca 100644 --- a/dev/src/DocGenerator/DocGenerator.php +++ b/dev/src/DocGenerator/DocGenerator.php @@ -32,16 +32,29 @@ class DocGenerator private $files; private $outputPath; private $executionPath; + private $componentId; + private $manifestPath; + private $release; /** * @param array $files */ - public function __construct(TypeGenerator $types, array $files, $outputPath, $executionPath) - { + public function __construct( + TypeGenerator $types, + array $files, + $outputPath, + $executionPath, + $componentId, + $manifestPath, + $release + ) { $this->types = $types; $this->files = $files; $this->outputPath = $outputPath; $this->executionPath = $executionPath; + $this->componentId = $componentId; + $this->manifestPath = $manifestPath; + $this->release = $release; } /** @@ -49,31 +62,56 @@ public function __construct(TypeGenerator $types, array $files, $outputPath, $ex * * @return void */ - public function generate() + public function generate($basePath, $pretty) { foreach ($this->files as $file) { - $currentFile = substr(str_replace($this->executionPath, '', $file), 3); + if ($basePath) { + $currentFileArr = explode($basePath, trim($file, '/')); + if (isset($currentFileArr[1])) { + $currentFile = trim($currentFileArr[1], '/'); + } + } + $isPhp = strrpos($file, '.php') == strlen($file) - strlen('.php'); if ($isPhp) { $fileReflector = new FileReflector($file); - $parser = new CodeParser($file, $currentFile, $fileReflector); + $parser = new CodeParser( + $file, + $currentFile, + $fileReflector, + dirname($this->executionPath), + $this->componentId, + $this->manifestPath, + $this->release + ); } else { $content = file_get_contents($file); - $parser = new MarkdownParser($currentFile, $content); + $split = explode('src/', $file); + $parser = new MarkdownParser($split[1], $content); } $document = $parser->parse(); - $writer = new Writer(json_encode($document), $this->outputPath); - $writer->write(substr($currentFile, 4)); + $writer = new Writer($document, $this->outputPath, $pretty); + $writer->write($currentFile); $this->types->addType([ 'id' => $document['id'], 'title' => $document['title'], - 'contents' => $document['id'] . '.json' + 'contents' => $this->prune($document['id'] . '.json') ]); } } + + private function prune($contentsFileName) + { + $explode = explode('/', $contentsFileName); + if (count($explode) > 1) { + array_shift($explode); + } + + return implode('/', $explode); + } } diff --git a/dev/src/DocGenerator/Parser/CodeParser.php b/dev/src/DocGenerator/Parser/CodeParser.php index e478bdd41d42..fd13c43f8885 100644 --- a/dev/src/DocGenerator/Parser/CodeParser.php +++ b/dev/src/DocGenerator/Parser/CodeParser.php @@ -18,6 +18,7 @@ namespace Google\Cloud\Dev\DocGenerator\Parser; use Google\Cloud\Dev\DocBlockStripSpaces; +use Google\Cloud\Dev\GetComponentsTrait; use phpDocumentor\Reflection\DocBlock; use phpDocumentor\Reflection\DocBlock\Description; use phpDocumentor\Reflection\DocBlock\Tag\SeeTag; @@ -25,21 +26,38 @@ class CodeParser implements ParserInterface { + use GetComponentsTrait; + const SNIPPET_NAME_REGEX = '/\/\/\s?\[snippet\=(\w{0,})\]/'; private $path; private $outputName; private $reflector; private $markdown; + private $projectRoot; private $externalTypes; - - public function __construct($path, $outputName, FileReflector $reflector) - { + private $componentId; + private $manifestPath; + private $release; + + public function __construct( + $path, + $outputName, + FileReflector $reflector, + $projectRoot, + $componentId, + $manifestPath, + $release + ) { $this->path = $path; $this->outputName = $outputName; $this->reflector = $reflector; $this->markdown = \Parsedown::instance(); + $this->projectRoot = $projectRoot; $this->externalTypes = json_decode(file_get_contents(__DIR__ .'/../../../../docs/external-classes.json'), true); + $this->componentId = $componentId; + $this->manifestPath = $manifestPath; + $this->release = $release; } public function parse() @@ -485,15 +503,59 @@ private function buildExternalType($type) private function buildLink($content) { - if ($content[0] === '\\') { - $content = substr($content, 1); + $componentId = null; + if (substr_compare(trim($content, '\\'), 'Google\Cloud', 0, 12) === 0) { + try { + $matches = []; + preg_match('/[Generator\<]?(Google\\\Cloud\\\[\w\\\]{0,})[\>]?[\[\]]?/', $content, $matches); + $ref = new \ReflectionClass($matches[1]); + } catch (\ReflectionException $e) { + throw new \Exception(sprintf( + 'Reflection Exception: %s in %s. Given class was %s', + $e->getMessage(), + realpath($this->path), + $content + )); + } + + $recurse = true; + $file = $ref->getFileName(); + + if (strpos($file, dirname(realpath($this->path))) !== false) { + $recurse = false; + } + + do { + $composer = dirname($file) .'/composer.json'; + if (file_exists($composer) && $component = $this->isComponent($composer)) { + $componentId = $component['id']; + if ($componentId === $this->componentId) { + $componentId = null; + } + $recurse = false; + } elseif (trim($file, '/') === trim($this->projectRoot, '/')) { + $recurse = false; + } else { + $file = dirname($file); + } + } while($recurse); } + $content = trim($content, '\\'); + $displayName = $content; $content = substr($content, 13); $parts = explode('::', $content); $type = strtolower(str_replace('\\', '/', $parts[0])); + if ($componentId) { + $version = ($this->release) + ? $this->getComponentVersion($this->manifestPath, $componentId) + : 'master'; + + $type = $componentId .'/'. $version .'/'. $type; + } + $openTag = ' $examples ]; } + + private static $composerFiles = []; + + private function isComponent($composerPath) + { + if (isset(self::$composerFiles[$composerPath])) { + $contents = self::$composerFiles[$composerPath]; + } else { + $contents = json_decode(file_get_contents($composerPath), true); + self::$composerFiles[$composerPath] = $contents; + } + + if (isset($contents['extra']['component'])) { + return $contents['extra']['component']; + } + + return false; + } } diff --git a/dev/src/DocGenerator/Parser/MarkdownParser.php b/dev/src/DocGenerator/Parser/MarkdownParser.php index 2599f80d4954..058dba85bbfe 100644 --- a/dev/src/DocGenerator/Parser/MarkdownParser.php +++ b/dev/src/DocGenerator/Parser/MarkdownParser.php @@ -49,7 +49,7 @@ public function parse() $body = $doc->getElementsByTagName('body')->item(0); return [ - 'id' => strtolower(substr($pathinfo['dirname'] .'/'. $pathinfo['filename'], 5)), + 'id' => strtolower(trim($pathinfo['dirname'] .'/'. $pathinfo['filename'], '/.')), 'type' => 'guide', 'title' => $heading->textContent, 'name' => $heading->textContent, diff --git a/dev/src/DocGenerator/TableOfContents.php b/dev/src/DocGenerator/TableOfContents.php new file mode 100644 index 000000000000..162a5877c8be --- /dev/null +++ b/dev/src/DocGenerator/TableOfContents.php @@ -0,0 +1,44 @@ +template = $template; + $this->component = $component; + $this->componentVersion = $componentVersion; + $this->outputPath = $outputPath; + } + + public function generate($pretty = false) + { + $toc = $this->template; + $toc['services'] = $this->component; + $toc['tagName'] = $this->componentVersion; + + $writer = new Writer($toc, $this->outputPath, $pretty); + $writer->write('toc.json'); + } +} diff --git a/dev/src/DocGenerator/TypeGenerator.php b/dev/src/DocGenerator/TypeGenerator.php index f05a39a6fcfc..8abfc8d4ac87 100644 --- a/dev/src/DocGenerator/TypeGenerator.php +++ b/dev/src/DocGenerator/TypeGenerator.php @@ -36,9 +36,9 @@ public function addType(array $type) $this->types[] = $type; } - public function write() + public function write($pretty = false) { - $writer = new Writer(json_encode($this->types), $this->outputPath); + $writer = new Writer($this->types, $this->outputPath, $pretty); $writer->write('types.json'); } } diff --git a/dev/src/DocGenerator/Writer.php b/dev/src/DocGenerator/Writer.php index 4cafcb9200e3..f2c22e458b63 100644 --- a/dev/src/DocGenerator/Writer.php +++ b/dev/src/DocGenerator/Writer.php @@ -21,11 +21,13 @@ class Writer { private $content; private $outputPath; + private $pretty; - public function __construct($content, $outputPath) + public function __construct(array $content, $outputPath, $pretty = false) { $this->content = $content; $this->outputPath = $outputPath; + $this->pretty = (bool) $pretty; } public function write($currentFile) @@ -33,10 +35,14 @@ public function write($currentFile) $path = $this->buildOutputPath($currentFile); if (!is_dir(dirname($path))) { - mkdir(dirname($path), 0777, true); + @mkdir(dirname($path), 0777, true); } - file_put_contents($path, $this->content); + $content = ($this->pretty) + ? json_encode($this->content, JSON_PRETTY_PRINT) + : json_encode($this->content); + + file_put_contents($path, $content); } private function buildOutputPath($currentFile) diff --git a/dev/src/GetComponentsTrait.php b/dev/src/GetComponentsTrait.php new file mode 100644 index 000000000000..3efa11290fe5 --- /dev/null +++ b/dev/src/GetComponentsTrait.php @@ -0,0 +1,116 @@ + 1) { + $component['prefix'] = dirname('src' . $path[1]); + } else { + $component['prefix'] = ''; + } + + $components[] = $component; + } + + return $components; + } + + private function getComponentVersion($manifestPath, $componentId) + { + $manifest = $this->getComponentManifest($manifestPath, $componentId); + return $manifest['versions'][0]; + } + + private function getComponentManifest($manifestPath, $componentId) + { + $manifest = $this->getManifest($manifestPath); + $index = $this->getManifestComponentModuleIndex($manifestPath, $manifest, $componentId); + + return $manifest['modules'][$index]; + } + + private function getManifestComponentModuleIndex($manifestPath, array $manifest, $componentId) + { + $modules = array_filter($this->getManifest($manifestPath)['modules'], function ($module) use ($componentId) { + return ($module['id'] === $componentId); + }); + + return array_keys($modules)[0]; + } + + private function getManifest($manifestPath) + { + if (self::$__manifest) { + $manifest = self::$__manifest; + } else { + $manifest = json_decode(file_get_contents($manifestPath), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new RuntimeException('Could not decode manifest json'); + } + + self::$__manifest = $manifest; + } + + return $manifest; + } + + private function getComponentComposer($componentId) + { + $components = $this->getComponents($this->components, $this->defaultComponentComposer); + + $components = array_values(array_filter($components, function ($component) use ($componentId) { + return ($component['id'] === $componentId); + })); + + if (count($components) === 0) { + throw new \InvalidArgumentException(sprintf( + 'Given component id %s is not a valid component.', + $componentId + )); + } + + return $components[0]; + } +} diff --git a/dev/src/Release/Command/Release.php b/dev/src/Release/Command/Release.php index edc3bd1f21c3..37e0b62400c8 100644 --- a/dev/src/Release/Command/Release.php +++ b/dev/src/Release/Command/Release.php @@ -17,20 +17,35 @@ namespace Google\Cloud\Dev\Release\Command; +use Google\Cloud\Dev\GetComponentsTrait; use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use vierbergenlars\SemVer\version; class Release extends Command { - const PATH_MANIFEST = 'docs/manifest.json'; - const PATH_SERVICE_BUILDER = 'src/ServiceBuilder.php'; + use GetComponentsTrait; + + const COMPONENT_BASE = '%s/../src'; + const DEFAULT_COMPONENT = 'google-cloud'; + const DEFAULT_COMPONENT_COMPOSER = '%s/../composer.json'; + const PATH_MANIFEST = '%s/../docs/manifest.json'; + const PATH_SERVICE_BUILDER = '%s/../src/ServiceBuilder.php'; private $cliBasePath; + private $defaultClient; + + private $manifest; + + private $defaultComponentComposer; + + private $components; + private $allowedReleaseTypes = [ 'major', 'minor', 'patch' ]; @@ -39,6 +54,11 @@ public function __construct($cliBasePath) { $this->cliBasePath = $cliBasePath; + $this->defaultClient = sprintf(self::PATH_SERVICE_BUILDER, $cliBasePath); + $this->manifest = sprintf(self::PATH_MANIFEST, $cliBasePath); + $this->defaultComponentComposer = sprintf(self::DEFAULT_COMPONENT_COMPOSER, $cliBasePath); + $this->components = sprintf(self::COMPONENT_BASE, $cliBasePath); + parent::__construct(); } @@ -46,23 +66,31 @@ protected function configure() { $this->setName('release') ->setDescription('Prepares a new release') - ->addArgument('version', InputArgument::REQUIRED, 'The new version number'); + ->addArgument('version', InputArgument::REQUIRED, 'The new version number.') + ->addOption( + 'component', + 'c', + InputOption::VALUE_REQUIRED, + 'The component for which the version should be updated.', + self::DEFAULT_COMPONENT + ); } protected function execute(InputInterface $input, OutputInterface $output) { + $component = $this->getComponentComposer($input->getOption('component')); + $version = $input->getArgument('version'); + + // If the version is one of "major", "minor" or "patch", determine the + // correct incrementation. if (in_array(strtolower($version), $this->allowedReleaseTypes)) { - $version = $this->getNextVersionName($version); + $version = $this->getNextVersionName($version, $component); } try { $validatedVersion = new version($version); } catch (\Exception $e) { - $validatedVersion = null; - } - - if (is_null($validatedVersion)) { throw new RuntimeException(sprintf( 'Given version %s is not a valid version name', $version @@ -72,18 +100,33 @@ protected function execute(InputInterface $input, OutputInterface $output) $version = (string) $validatedVersion; $output->writeln(sprintf( - 'Adding version %s to Documentation Manifest.', - $version + 'Adding version %s to Documentation Manifest for component %s.', + $version, + $component['id'] )); - $this->addToManifest($version); + $this->addToComponentManifest($version, $component); $output->writeln(sprintf( - 'Setting ServiceBuilder version constant to %s.', + 'Setting component version constant to %s.', $version )); - $this->updateServiceBuilder($version); + $this->updateComponentVersionConstant($version, $component); + $output->writeln(sprintf( + 'File %s VERSION constant updated to %s', + $component['entry'], + $version + )); + + if ($component['id'] !== 'google-cloud') { + $this->updateComponentVersionFile($version, $component); + $output->writeln(sprintf( + 'Component %s VERSION file updated to %s', + $component['id'], + $version + )); + } $output->writeln(sprintf( 'Release %s generated!', @@ -91,64 +134,72 @@ protected function execute(InputInterface $input, OutputInterface $output) )); } - private function getNextVersionName($type) + private function getNextVersionName($type, array $component) { - $manifest = $this->getManifest(); - $lastRelease = new version($manifest['versions'][0]); + $manifest = $this->getComponentManifest($this->manifest, $component['id']); + + if ($manifest['versions'][0] === 'master') { + $lastRelease = new version('0.0.0'); + } else { + $lastRelease = new version($manifest['versions'][0]); + } return $lastRelease->inc($type); } - private function addToManifest($version) + private function addToComponentManifest($version, array $component) { - $manifest = $this->getManifest(); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new RuntimeException('Could not decode manifest json'); - } + $manifest = $this->getManifest($this->manifest); + $index = $this->getManifestComponentModuleIndex($this->manifest, $manifest, $component['id']); - array_unshift($manifest['versions'], 'v'. $version); + array_unshift($manifest['modules'][$index]['versions'], 'v'. $version); $content = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ."\n"; - $result = file_put_contents($this->getManifestPath(), $content); + $result = file_put_contents($this->manifest, $content); if (!$result) { throw new RuntimeException('File write failed'); } } - private function updateServiceBuilder($version) + private function updateComponentVersionConstant($version, array $component) { - $path = $this->cliBasePath .'/../'. self::PATH_SERVICE_BUILDER; + if (is_null($component['entry'])) { + return false; + } + + $path = $this->cliBasePath .'/../'. $component['path'] .'/'. $component['entry']; if (!file_exists($path)) { - throw new RuntimeException('ServiceBuilder not found at '. $path); + throw new \RuntimeException(sprintf( + 'Component entry file %s does not exist', + $path + )); } - $sb = file_get_contents($path); + $entry = file_get_contents($path); $replacement = sprintf("const VERSION = '%s';", $version); - $sb = preg_replace("/const VERSION = '[0-9.]{0,}'\;/", $replacement, $sb); + $entry = preg_replace("/const VERSION = [\'\\\"]([0-9.]{0,}|master)[\'\\\"]\;/", $replacement, $entry); - $result = file_put_contents($path, $sb); + $result = file_put_contents($path, $entry); if (!$result) { throw new RuntimeException('File write failed'); } + + return true; } - private function getManifest() + private function updateComponentVersionFile($version, array $component) { - $path = $this->getManifestPath(); - if (!file_exists($path)) { - throw new RuntimeException('Manifest file not found at '. $path); - } + $path = $this->cliBasePath .'/../'. $component['path'] .'/VERSION'; + $result = file_put_contents($path, $version); - return json_decode(file_get_contents($path), true); - } + if (!$result) { + throw new RuntimeException('File write failed'); + } - private function getManifestPath() - { - return $this->cliBasePath .'/../'. self::PATH_MANIFEST; + return true; } } diff --git a/dev/src/Snippet/Parser/Snippet.php b/dev/src/Snippet/Parser/Snippet.php index 74316ecaf25d..d1ace59affdf 100644 --- a/dev/src/Snippet/Parser/Snippet.php +++ b/dev/src/Snippet/Parser/Snippet.php @@ -152,9 +152,14 @@ public function invoke($returnVar = null) $cb = function($return) use ($content) { extract($this->locals); - ob_start(); - $res = eval($content ."\n\n". $return); - $out = ob_get_clean(); + try { + ob_start(); + $res = eval($content ."\n\n". $return); + $out = ob_get_clean(); + } catch (\Exception $e) { + ob_end_clean(); + throw $e; + } return new InvokeResult($res, $out); }; diff --git a/dev/src/Split/Command/Split.php b/dev/src/Split/Command/Split.php new file mode 100644 index 000000000000..6959e6985ac7 --- /dev/null +++ b/dev/src/Split/Command/Split.php @@ -0,0 +1,172 @@ +cliBasePath = $cliBasePath; + $this->splitShell = sprintf(self::SPLIT_SHELL, $cliBasePath); + $this->components = sprintf(self::COMPONENT_BASE, $cliBasePath); + $this->manifest = sprintf(self::PATH_MANIFEST, $cliBasePath); + + $this->http = new Client; + $this->token = getenv(self::TOKEN_ENV); + + parent::__construct(); + } + + protected function configure() + { + $this->setName('split') + ->setDescription('Split subtree and push to various remotes.'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + if (!getenv(self::TAG_ENV)) { + $output->writeln('This command should only be run inside a CI post-release process'); + return; + } + + $components = $this->getComponents($this->components); + + $tag = getenv(self::TAG_ENV); + + $parentTagSource = sprintf(self::PARENT_TAG_NAME, $tag); + + foreach ($components as $component) { + $output->writeln(''); + $output->writeln(sprintf('Starting on component %s', $component['id'])); + $output->writeln('------------'); + shell_exec(sprintf( + '%s %s %s', + $this->splitShell, + $component['prefix'], + $component['target'] + )); + + $target = $component['target']; + $matches = []; + preg_match(self::TARGET_REGEX, $target, $matches); + + $org = $matches[1]; + $repo = $matches[2]; + + $version = $this->getComponentVersion($this->manifest, $component['id']); + try { + new version($version); + } catch (SemVerException $e) { + $output->writeln(sprintf( + 'Component %s version %s is invalid.', + $component['id'], + $version + )); + continue; + } + + if ($this->doesTagExist($version, $org, $repo)) { + $output->writeln(sprintf( + 'Component %s already tagged at version %s', + $component['id'], + $version + )); + continue; + } + + $name = $component['displayName'] .' '. $version; + $notes = sprintf( + 'For release notes, please see the [associated Google Cloud PHP release](%s).', + $parentTagSource + ); + $this->createRelease($version, $org, $repo, $name, $notes); + + $output->writeln(sprintf( + 'Release %s created for component %s', + $version, + $component['id'] + )); + } + } + + private function doesTagExist($tagName, $org, $repo) + { + $res = $this->http->get(sprintf( + self::GITHUB_RELEASES_ENDPOINT, + $org, $repo, $tagName + ), [ + 'http_errors' => false, + 'auth' => [null, $this->token] + ]); + + return ($res->getStatusCode() === 200); + } + + private function createRelease($tagName, $org, $repo, $name, $notes) + { + $requestBody = [ + 'tag_name' => $tagName, + 'name' => $name, + 'body' => $notes + ]; + + $res = $this->http->post(sprintf( + self::GITHUB_RELEASE_CREATE_ENDPOINT, + $org, $repo + ), [ + 'http_errors' => false, + 'json' => $requestBody, + 'auth' => [null, $this->token] + ]); + } +} diff --git a/docs/contents/cloud-bigquery.json b/docs/contents/cloud-bigquery.json new file mode 100644 index 000000000000..1d92ef396619 --- /dev/null +++ b/docs/contents/cloud-bigquery.json @@ -0,0 +1,31 @@ +[{ + "title": "BigQueryClient", + "type": "bigquery/bigqueryclient" +}, { + "title": "Bytes", + "type": "bigquery/bytes" +}, { + "title": "Dataset", + "type": "bigquery/dataset" +}, { + "title": "Date", + "type": "bigquery/date" +}, { + "title": "InsertResponse", + "type": "bigquery/insertresponse" +}, { + "title": "Job", + "type": "bigquery/job" +}, { + "title": "QueryResults", + "type": "bigquery/queryresults" +}, { + "title": "Table", + "type": "bigquery/table" +}, { + "title": "Time", + "type": "bigquery/time" +}, { + "title": "Timestamp", + "type": "bigquery/timestamp" +}] diff --git a/docs/contents/cloud-core.json b/docs/contents/cloud-core.json new file mode 100644 index 000000000000..9779bf4b5c63 --- /dev/null +++ b/docs/contents/cloud-core.json @@ -0,0 +1,30 @@ +[{ + "title": "Overview", + "type": "core/readme" +}, { + "title": "IAM", + "type": "core/iam/iam", + "patterns": [ + "core/iam/\\w{1,}" + ], + "nav": [{ + "title": "PolicyBuilder", + "type": "core/iam/policybuilder" + }] +}, { + "title": "Upload", + "type": "core/upload/abstractuploader", + "nav": [{ + "title": "MultipartUploader", + "type": "core/upload/multipartuploader" + }, { + "title": "ResumableUploader", + "type": "core/upload/resumableuploader" + }, { + "title": "StreamableUploader", + "type": "core/upload/streamableuploader" + }] +}, { + "title": "Int64", + "type": "core/int64" +}] diff --git a/docs/contents/cloud-datastore.json b/docs/contents/cloud-datastore.json new file mode 100644 index 000000000000..aec83962d0d2 --- /dev/null +++ b/docs/contents/cloud-datastore.json @@ -0,0 +1,25 @@ +[{ + "title": "DatastoreClient", + "type": "datastore/datastoreclient" +}, { + "title": "Transaction", + "type": "datastore/transaction" +}, { + "title": "Entity", + "type": "datastore/entity" +}, { + "title": "Key", + "type": "datastore/key" +}, { + "title": "Query", + "type": "datastore/query/query" +}, { + "title": "GQL Query", + "type": "datastore/query/gqlquery" +}, { + "title": "GeoPoint", + "type": "datastore/geopoint" +}, { + "title": "Blob", + "type": "datastore/blob" +}] diff --git a/docs/contents/cloud-error-reporting.json b/docs/contents/cloud-error-reporting.json new file mode 100644 index 000000000000..a6f2d22170da --- /dev/null +++ b/docs/contents/cloud-error-reporting.json @@ -0,0 +1,20 @@ +[{ + "title": "Overview", + "type": "errorreporting/readme" +}, { + "title": "v1beta1", + "type": "errorreporting/v1beta1/readme", + "patterns": [ + "errorreporting/v1beta1/\\w{1,}" + ], + "nav": [{ + "title": "ErrorGroupServiceClient", + "type": "errorreporting/v1beta1/errorgroupserviceclient" + }, { + "title": "ErrorStatsServiceClient", + "type": "errorreporting/v1beta1/errorstatsserviceclient" + }, { + "title": "ReportErrorsServiceClient", + "type": "errorreporting/v1beta1/reporterrorsserviceclient" + }] +}] diff --git a/docs/contents/cloud-logging.json b/docs/contents/cloud-logging.json new file mode 100644 index 000000000000..8b40def8299f --- /dev/null +++ b/docs/contents/cloud-logging.json @@ -0,0 +1,35 @@ +[{ + "title": "LoggingClient", + "type": "logging/loggingclient" +}, { + "title": "Entry", + "type": "logging/entry" +}, { + "title": "Logger", + "type": "logging/logger" +}, { + "title": "Metric", + "type": "logging/metric" +},{ + "title": "PsrLogger", + "type": "logging/psrlogger" +},{ + "title": "Sink", + "type": "logging/sink" +}, { + "title": "v2", + "type": "logging/v2/readme", + "patterns": [ + "logging/v2/\\w{1,}" + ], + "nav": [{ + "title": "ConfigServiceV2Client", + "type": "logging/v2/configservicev2client" + }, { + "title": "LoggingServiceV2Client", + "type": "logging/v2/loggingservicev2client" + }, { + "title": "MetricsServiceV2Client", + "type": "logging/v2/metricsservicev2client" + }] +}] diff --git a/docs/contents/cloud-monitoring.json b/docs/contents/cloud-monitoring.json new file mode 100644 index 000000000000..092ed41613bc --- /dev/null +++ b/docs/contents/cloud-monitoring.json @@ -0,0 +1,17 @@ +[{ + "title": "Overview", + "type": "monitoring/readme" +}, { + "title": "v3", + "type": "monitoring/v3/readme", + "patterns": [ + "monitoring/v3/\\w{1,}" + ], + "nav": [{ + "title": "GroupServiceClient", + "type": "monitoring/v3/groupserviceclient" + }, { + "title": "MetricServiceClient", + "type": "monitoring/v3/metricserviceclient" + }] +}] diff --git a/docs/contents/cloud-natural-language.json b/docs/contents/cloud-natural-language.json new file mode 100644 index 000000000000..c9d82e7b35cd --- /dev/null +++ b/docs/contents/cloud-natural-language.json @@ -0,0 +1,7 @@ +[{ + "title": "NaturalLanguageClient", + "type": "naturallanguage/naturallanguageclient" +}, { + "title": "Annotation", + "type": "naturallanguage/annotation" +}] diff --git a/docs/contents/cloud-pubsub.json b/docs/contents/cloud-pubsub.json new file mode 100644 index 000000000000..ef5acc571971 --- /dev/null +++ b/docs/contents/cloud-pubsub.json @@ -0,0 +1,26 @@ +[{ + "title": "PubSubClient", + "type": "pubsub/pubsubclient" +}, { + "title": "Message", + "type": "pubsub/message" +}, { + "title": "Subscription", + "type": "pubsub/subscription" +}, { + "title": "Topic", + "type": "pubsub/topic" +}, { + "title": "v1", + "type": "pubsub/v1/readme", + "patterns": [ + "pubsub/v1/\\w{1,}" + ], + "nav": [{ + "title": "PublisherClient", + "type": "pubsub/v1/publisherclient" + }, { + "title": "SubscriberClient", + "type": "pubsub/v1/subscriberclient" + }] +}] diff --git a/docs/contents/cloud-speech.json b/docs/contents/cloud-speech.json new file mode 100644 index 000000000000..d433dce34a69 --- /dev/null +++ b/docs/contents/cloud-speech.json @@ -0,0 +1,17 @@ +[{ + "title": "SpeechClient", + "type": "speech/speechclient" +}, { + "title": "Operation", + "type": "speech/operation" +}, { + "title": "v1beta1", + "type": "speech/v1beta1/readme", + "patterns": [ + "speech/v1beta1/\\w{1,}" + ], + "nav": [{ + "title": "SpeechClient", + "type": "speech/v1beta1/speechclient" + }] +}] diff --git a/docs/contents/cloud-storage.json b/docs/contents/cloud-storage.json new file mode 100644 index 000000000000..0b7c36cbfd68 --- /dev/null +++ b/docs/contents/cloud-storage.json @@ -0,0 +1,13 @@ +[{ + "title": "StorageClient", + "type": "storage/storageclient" +}, { + "title": "ACL", + "type": "storage/acl" +}, { + "title": "Bucket", + "type": "storage/bucket" +}, { + "title": "StorageObject", + "type": "storage/storageobject" +}] diff --git a/docs/contents/cloud-translate.json b/docs/contents/cloud-translate.json new file mode 100644 index 000000000000..d829deaa0161 --- /dev/null +++ b/docs/contents/cloud-translate.json @@ -0,0 +1,4 @@ +[{ + "title": "TranslateClient", + "type": "translate/translateclient" +}] diff --git a/docs/contents/cloud-vision.json b/docs/contents/cloud-vision.json new file mode 100644 index 000000000000..1ffef7a6ec5f --- /dev/null +++ b/docs/contents/cloud-vision.json @@ -0,0 +1,34 @@ +[{ + "title": "VisionClient", + "type": "vision/visionclient" +}, { + "title": "Image", + "type": "vision/image" +}, { + "title": "Annotation", + "type": "vision/annotation", + "nav": [ + { + "title": "CropHint", + "type": "vision/annotation/crophint" + }, { + "title": "Document", + "type": "vision/annotation/document" + }, { + "title": "Entity", + "type": "vision/annotation/entity" + }, { + "title": "Face", + "type": "vision/annotation/face" + }, { + "title": "ImageProperties", + "type": "vision/annotation/imageproperties" + }, { + "title": "SafeSearch", + "type": "vision/annotation/safesearch" + }, { + "title": "Web", + "type": "vision/annotation/web" + } + ] +}] diff --git a/docs/contents/google-cloud.json b/docs/contents/google-cloud.json new file mode 100644 index 000000000000..ab88991df84b --- /dev/null +++ b/docs/contents/google-cloud.json @@ -0,0 +1,4 @@ +[{ + "title": "ServiceBuilder", + "type": "servicebuilder" +}] diff --git a/docs/home.html b/docs/home.html index 46eb81f7d874..82f9296f1687 100644 --- a/docs/home.html +++ b/docs/home.html @@ -22,7 +22,7 @@