diff --git a/Core/src/OptionsValidator.php b/Core/src/OptionsValidator.php index cb82ad4bb1ac..2fb2e157e83b 100644 --- a/Core/src/OptionsValidator.php +++ b/Core/src/OptionsValidator.php @@ -106,4 +106,56 @@ public function validateOptions(array $options, array|Message|string ...$optionT return $splitOptions; } + + /** + * @var array + */ + private static array $allowedKeysCache = []; + + /** + * Filter an array of options to only include those matching the supplied `$optionTypes`. + * + * @param array $options + * @param array|Message|string ...$optionTypes + * @return array + */ + public function stripUnknownOptions(array $options, array|Message|string ...$optionTypes): array + { + $cacheKey = serialize(array_map(function ($type) { + return is_object($type) ? get_class($type) : $type; + }, $optionTypes)); + + if (isset(self::$allowedKeysCache[$cacheKey])) { + return array_intersect_key($options, array_flip(self::$allowedKeysCache[$cacheKey])); + } + + $allowedKeys = []; + foreach ($optionTypes as $optionType) { + if (is_array($optionType)) { + $allowedKeys = array_merge($allowedKeys, $optionType); + } elseif ($optionType === CallOptions::class) { + $callOptionKeys = array_keys((new CallOptions([]))->toArray()); + $allowedKeys = array_merge($allowedKeys, $callOptionKeys); + } elseif ($optionType instanceof Message + || (is_string($optionType) && is_subclass_of($optionType, Message::class)) + ) { + $messageKeys = array_map( + fn ($method) => lcfirst(substr($method, 3)), + array_filter( + get_class_methods($optionType), + fn ($m) => 0 === strpos($m, 'get') + ) + ); + $allowedKeys = array_merge($allowedKeys, $messageKeys); + } elseif (is_string($optionType)) { + $allowedKeys[] = $optionType; + } else { + throw new LogicException(sprintf('Invalid option type: %s', $optionType)); + } + } + + self::$allowedKeysCache[$cacheKey] = $allowedKeys; + + return array_intersect_key($options, array_flip($allowedKeys)); + } } diff --git a/Core/tests/Unit/DummyMessage.php b/Core/tests/Unit/DummyMessage.php new file mode 100644 index 000000000000..fb0e9d650d89 --- /dev/null +++ b/Core/tests/Unit/DummyMessage.php @@ -0,0 +1,40 @@ +validator = new OptionsValidator(); + } + + public function testStripUnknownOptions() + { + $options = [ + 'parameters' => ['a' => 1], + 'queryOptions' => ['optimizerVersion' => '1'], + 'requestOptions' => ['priority' => 1], + 'timeoutMillis' => 100, + 'unknown' => 'strip me' + ]; + + $stripped = $this->validator->stripUnknownOptions( + $options, + ['parameters'], + CallOptions::class, + DummyMessage::class + ); + + $this->assertArrayHasKey('parameters', $stripped); + $this->assertArrayHasKey('queryOptions', $stripped); // From DummyMessage + $this->assertArrayHasKey('requestOptions', $stripped); // From DummyMessage + $this->assertArrayHasKey('timeoutMillis', $stripped); // From CallOptions + $this->assertArrayNotHasKey('unknown', $stripped); + } +} diff --git a/Spanner/composer.json b/Spanner/composer.json index a558ba9a6504..ad0b8cfb3eb9 100644 --- a/Spanner/composer.json +++ b/Spanner/composer.json @@ -6,7 +6,7 @@ "require": { "php": "^8.1", "ext-grpc": "*", - "google/cloud-core": "^1.68", + "google/cloud-core": "^1.73.0", "google/gax": "^1.41.0", "google/cloud-monitoring": "^2.2", "open-telemetry/sdk": "^1.13" @@ -21,7 +21,8 @@ "google/cloud-pubsub": "^2.0", "dg/bypass-finals": "^1.7", "dms/phpunit-arraysubset-asserts": "^0.5.0", - "symfony/process": "^6.4" + "symfony/process": "^6.4", + "nikic/php-parser": "^5.0" }, "suggest": { "ext-protobuf": "Provides a significant increase in throughput over the pure PHP protobuf implementation. See https://cloud.google.com/php/grpc for installation instructions.", diff --git a/Spanner/src/Database.php b/Spanner/src/Database.php index 4961a60f8b6e..e1b38bc0790c 100644 --- a/Spanner/src/Database.php +++ b/Spanner/src/Database.php @@ -738,7 +738,6 @@ public function iam(): IamManager * up front. Instead, the transaction will be considered * "single-use", and may be used for only a single operation. * **Defaults to** `false`. - * @type array $sessionOptions Session configuration and request options. * Session labels may be applied using the `labels` key. * } * @return TransactionalReadInterface @@ -791,7 +790,6 @@ public function snapshot(array $options = []): TransactionalReadInterface * up front. Instead, the transaction will be considered * "single-use", and may be used for only a single operation. * **Defaults to** `false`. - * @type array $sessionOptions Session configuration and request options. * Session labels may be applied using the `labels` key. * @type string $tag A transaction tag. Requests made using this transaction will * use this as the transaction tag. @@ -892,7 +890,6 @@ public function transaction(array $options = []): Transaction * that in a single-use transaction, only a single operation may * be executed, and rollback is not available. **Defaults to** * `false`. - * @type array $sessionOptions Session configuration and request options. * Session labels may be applied using the `labels` key. * @type string $tag A transaction tag. Requests made using this transaction will * use this as the transaction tag. @@ -1018,6 +1015,10 @@ public function runTransaction(callable $operation, array $options = []): mixed * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1067,6 +1068,10 @@ public function insert(string $table, array $data, array $options = []): Timesta * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1113,6 +1118,10 @@ public function insertBatch(string $table, array $dataSet, array $options = []): * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1159,6 +1168,10 @@ public function update(string $table, array $data, array $options = []): Timesta * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1206,6 +1219,10 @@ public function updateBatch(string $table, array $dataSet, array $options = []): * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1254,6 +1271,10 @@ public function insertOrUpdate(string $table, array $data, array $options = []): * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1301,6 +1322,10 @@ public function insertOrUpdateBatch(string $table, array $dataSet, array $option * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1349,6 +1374,10 @@ public function replace(string $table, array $data, array $options = []): Timest * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1399,6 +1428,10 @@ public function replaceBatch(string $table, array $dataSet, array $options = []) * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for single-use * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to be used with the request. + * @type array $transportOptions Transport options to be used with the request. * } * @return Timestamp The commit Timestamp. */ @@ -1632,7 +1665,6 @@ public function delete(string $table, KeySet $keySet, array $options = []): Time * chosen, any snapshot options will be disregarded. If `$begin` * is false, transaction type MUST be `Database::CONTEXT_READ`. * **Defaults to** `Database::CONTEXT_READ`. - * @type array $sessionOptions Session configuration and request options. * Session labels may be applied using the `labels` key. * @type array $queryOptions Query optimizer configuration. * @type string $queryOptions.optimizerVersion An option to control the @@ -1654,6 +1686,12 @@ public function delete(string $table, KeySet $keySet, array $options = []): Time * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for read-only * transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout in milliseconds to be used for the API request. + * @type array $transportOptions Transport options to be used with the request. + * For more information on available call options, please see + * [CallOptions](https://docs.cloud.google.com/php/docs/reference/gax/latest/Options.CallOptions). * @type array $directedReadOptions Directed read options. * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} * If using the `replicaSelection::type` setting, utilize the constants available in @@ -1675,13 +1713,18 @@ public function execute($sql, array $options = []): Result ); $session = $options['session'] ?? $this->session; - $executeOptions = $this->pluckArray(['parameters', 'types'], $options); - return $this->operation->execute($session, $sql, $executeOptions + [ - 'transaction' => $txnOptions, - 'transactionContext' => $txnContext, - 'directedReadOptions' => $directedReadOptions, - 'route-to-leader' => $txnContext === Database::CONTEXT_READWRITE - ]); + $executeOptions = $this->optionsValidator->stripUnknownOptions( + $options, + ['parameters', 'types'], + \Google\ApiCore\Options\CallOptions::class, + \Google\Cloud\Spanner\V1\ExecuteSqlRequest::class + ); + $executeOptions['transaction'] = $txnOptions; + $executeOptions['transactionContext'] = $txnContext; + $executeOptions['directedReadOptions'] = $directedReadOptions; + $executeOptions['route-to-leader'] = $txnContext === Database::CONTEXT_READWRITE; + + return $this->operation->execute($session, $sql, $executeOptions); } /** @@ -1915,10 +1958,10 @@ public function executePartitionedUpdate($statement, array $options = []): int $transaction = $this->operation->transaction($this->session, $beginTransactionOptions); - return $this->operation->executeUpdate($this->session, $transaction, $statement, [ - 'statsItem' => 'rowCountLowerBound', - 'route-to-leader' => true, - ] + $options); + $options['statsItem'] = 'rowCountLowerBound'; + $options['route-to-leader'] = true; + + return $this->operation->executeUpdate($this->session, $transaction, $statement, $options); } /** @@ -2022,7 +2065,6 @@ public function executePartitionedUpdate($statement, array $options = []): int * chosen, any snapshot options will be disregarded. If `$begin` * is false, transaction type MUST be `Database::CONTEXT_READ`. * **Defaults to** `Database::CONTEXT_READ`. - * @type array $sessionOptions Session configuration and request options. * Session labels may be applied using the `labels` key. * @type array $requestOptions Request options. * For more information on available options, please see @@ -2030,6 +2072,12 @@ public function executePartitionedUpdate($statement, array $options = []): int * Please note, if using the `priority` setting you may utilize the constants available * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as it is not supported for read-only transactions. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout in milliseconds to be used for the API request. + * @type array $transportOptions Transport options to be used with the request. + * For more information on available call options, please see + * [CallOptions](https://docs.cloud.google.com/php/docs/reference/gax/latest/Options.CallOptions). * @type array $directedReadOptions Directed read options. * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} * If using the `replicaSelection::type` setting, utilize the constants available in @@ -2048,22 +2096,20 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] { [$txnOptions, $txnContext] = $this->transactionOptionsBuilder->transactionSelector($options); - $readOptions = $this->pluckArray( - ['index', 'limit', 'orderBy', 'lockHint', 'directedReadOptions'], - $options + $readOptions = $this->optionsValidator->stripUnknownOptions( + $options, + \Google\ApiCore\Options\CallOptions::class, + \Google\Cloud\Spanner\V1\ReadRequest::class ); - $readOptions += [ - 'transactionContext' => $txnContext, - 'directedReadOptions' => $this->transactionOptionsBuilder->configureDirectedReadOptions( - ['transaction' => $txnOptions] + $readOptions, - $this->directedReadOptions - ), - 'transaction' => $txnOptions, - ]; + $readOptions['transactionContext'] = $txnContext; + $readOptions['directedReadOptions'] = $this->transactionOptionsBuilder->configureDirectedReadOptions( + ['transaction' => $txnOptions] + $readOptions, + $this->directedReadOptions + ); + $readOptions['transaction'] = $txnOptions; - return $this->operation->read($this->session, $table, $keySet, $columns, $readOptions + [ - 'route-to-leader' => $txnContext === Database::CONTEXT_READ - ]); + $readOptions['route-to-leader'] = $txnContext === Database::CONTEXT_READ; + return $this->operation->read($this->session, $table, $keySet, $columns, $readOptions); } /** diff --git a/Spanner/src/Operation.php b/Spanner/src/Operation.php index 14fb32488915..27d8e26c2941 100644 --- a/Spanner/src/Operation.php +++ b/Spanner/src/Operation.php @@ -472,13 +472,18 @@ public function executeUpdateBatch( /** * @var ExecuteBatchDmlRequest $dmlRequest * @var array $callOptions + * @var array $rtl */ - [$dmlRequest, $callOptions] = $this->validateOptions( + [$dmlRequest, $callOptions, $_, $rtl] = $this->validateOptions( $options, new ExecuteBatchDmlRequest(), - CallOptions::class + CallOptions::class, + [], + ['route-to-leader'] ); + $callOptions += $rtl; + $response = $this->spannerClient->executeBatchDml($dmlRequest, $callOptions + [ 'resource-prefix' => $this->getDatabaseNameFromSession($session), 'route-to-leader' => $this->routeToLeader @@ -853,6 +858,10 @@ public function snapshot(SessionCache $session, array $options = []): Transactio * the type should be given as an array, where the first element * is `Database::TYPE_ARRAY` and the second element is the * array type, for instance `[Database::TYPE_ARRAY, Database::TYPE_INT64]`. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to use for this call. + * @type array $transportOptions Options to be used for the transport. * } * @return QueryPartition[] */ @@ -895,7 +904,11 @@ public function partitionQuery( ]); $partitions = []; - $queryPartitionOptions = $this->pluckArray(['parameters', 'types', 'maxPartitions', 'partitionSizeBytes'], $options); + $queryPartitionOptions = $this->optionsValidator->stripUnknownOptions( + $options, + ['parameters', 'types', 'maxPartitions', 'partitionSizeBytes'], + \Google\ApiCore\Options\CallOptions::class + ); /** @var RepeatedField $protoPartitions */ $protoPartitions = $response->getPartitions(); @@ -931,6 +944,10 @@ public function partitionQuery( * each partition may be smaller or larger than this size request. * **Defaults to** `1000000000` (i.e. 1 GiB). * @type string $index The name of an index on the table. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to use for this call. + * @type array $transportOptions Options to be used for the transport. * } * @return ReadPartition[] */ @@ -973,7 +990,11 @@ public function partitionRead( ]); $partitions = []; - $readPartitionOptions = $this->pluckArray(['index', 'maxPartitions', 'partitionSizeBytes'], $options); + $readPartitionOptions = $this->optionsValidator->stripUnknownOptions( + $options, + ['index', 'maxPartitions', 'partitionSizeBytes'], + \Google\ApiCore\Options\CallOptions::class + ); /** @var RepeatedField $protoPartitions */ $protoPartitions = $response->getPartitions(); foreach ($protoPartitions as $partition) { diff --git a/Spanner/src/Transaction.php b/Spanner/src/Transaction.php index 9a262d1b0d42..4acd21aadea9 100644 --- a/Spanner/src/Transaction.php +++ b/Spanner/src/Transaction.php @@ -235,6 +235,10 @@ public function getCommitStats(): CommitStats|null * @type array $transaction a set of Options for a transaction selector. * For more details on these options please * {@see https://cloud.google.com/spanner/docs/reference/rest/v1/TransactionSelector} + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to use for this call. + * @type array $transportOptions Options to be used for the transport. * } * @return int The number of rows modified. * @throws ValidationException @@ -261,7 +265,7 @@ public function executeUpdate(string $sql, array $options = []): int $this->type = self::TYPE_PRE_ALLOCATED; } - $options = $this->buildUpdateOptions($options); + $options = $this->buildUpdateOptions($options, \Google\Cloud\Spanner\V1\ExecuteSqlRequest::class); return $this->operation ->executeUpdate($this->session, $this, $sql, $options); } @@ -345,13 +349,17 @@ public function executeUpdate(string $sql, array $options = []): int * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `transactionTag` setting will be ignored as the transaction tag should have already * been set when creating the transaction. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to use for this call. + * @type array $transportOptions Options to be used for the transport. * } * @return BatchDmlResult * @throws \InvalidArgumentException If any statement is missing the `sql` key. */ public function executeUpdateBatch(array $statements, array $options = []): BatchDmlResult { - $options = $this->buildUpdateOptions($options); + $options = $this->buildUpdateOptions($options, \Google\Cloud\Spanner\V1\ExecuteBatchDmlRequest::class); return $this->operation->executeUpdateBatch( $this->session, $this, @@ -424,6 +432,12 @@ public function rollback(array $options = []): void * Please note, if using the `priority` setting you may utilize the constants available * on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value. * Please note, the `requestTag` setting will be ignored as it is not supported for commit requests. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout in milliseconds to be used for the API request. + * @type array $transportOptions Transport options to be used with the request. + * For more information on available call options, please see + * [CallOptions](https://docs.cloud.google.com/php/docs/reference/gax/latest/Options.CallOptions). * } * @return Timestamp The commit timestamp. * @throws \BadMethodCallException If the transaction is not active or already used. @@ -463,14 +477,24 @@ public function commit(array $options = []): Timestamp } // Build the commit options - $commitOptions = $this->pluckArray( - ['returnCommitStats', 'maxCommitDelay'], - $options + $commitOptions = (new \Google\Cloud\Core\OptionsValidator())->stripUnknownOptions( + $options, + \Google\ApiCore\Options\CallOptions::class, + \Google\Cloud\Spanner\V1\CommitRequest::class ); // Set the latest received precommit token from the last request from within this transaction. if ($this->precommitToken) { $commitOptions['precommitToken'] = $this->precommitToken; } + if (isset($commitOptions['requestOptions']) + && is_array($commitOptions['requestOptions']) + && isset($commitOptions['requestOptions']['requestTag']) + ) { + unset($commitOptions['requestOptions']['requestTag']); + if (0 === count($commitOptions['requestOptions'])) { + unset($commitOptions['requestOptions']); + } + } // set the transaction tag if it exists if ($this->tag) { $commitOptions['requestOptions']['transactionTag'] = $this->tag; @@ -556,10 +580,11 @@ public function setPrecommitToken(MultiplexedSessionPrecommitToken $precommitTok /** * Build the update options. * - * @param array $options The update options + * @param array $options The update options + * @param string $requestClass The protobuf request class * @return array */ - private function buildUpdateOptions(array $options): array + private function buildUpdateOptions(array $options, string $requestClass): array { $options['transactionType'] = $this->context; if (empty($this->transactionId) && isset($this->transactionSelector['begin'])) { @@ -569,16 +594,15 @@ private function buildUpdateOptions(array $options): array } [$txnOptions] = $this->transactionOptionsBuilder->transactionSelector($options); - $updateOptions = $this->pluckArray(['parameters', 'types'], $options); - $updateOptions += [ - 'seqno' => $this->seqno++, - 'transaction' => $txnOptions, - 'headers' => ['spanner-route-to-leader' => ['true']] - ]; - - if (isset($options['requestOptions'])) { - $updateOptions['requestOptions'] = $options['requestOptions']; - } + $updateOptions = (new \Google\Cloud\Core\OptionsValidator())->stripUnknownOptions( + $options, + $requestClass === \Google\Cloud\Spanner\V1\ExecuteSqlRequest::class ? ['parameters', 'types'] : [], + \Google\ApiCore\Options\CallOptions::class, + $requestClass + ); + $updateOptions['seqno'] = $this->seqno++; + $updateOptions['transaction'] = $txnOptions; + $updateOptions['route-to-leader'] = true; if ($this->tag) { $updateOptions['requestOptions']['transactionTag'] = $this->tag; diff --git a/Spanner/src/TransactionalReadTrait.php b/Spanner/src/TransactionalReadTrait.php index f2598b195d25..1cefc9463e57 100644 --- a/Spanner/src/TransactionalReadTrait.php +++ b/Spanner/src/TransactionalReadTrait.php @@ -240,6 +240,10 @@ trait TransactionalReadTrait * If using the `replicaSelection::type` setting, utilize the constants available in * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * @type string $partitionToken + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to use for this call. + * @type array $transportOptions Options to be used for the transport. * } * @codingStandardsIgnoreEnd * @return Result @@ -264,9 +268,11 @@ public function execute(string $sql, array $options = []): Result $this->directedReadOptions ); - $executeSqlOptions = $this->pluckArray( - ['partitionToken', 'parameters', 'types', ], - $options + $executeSqlOptions = (new \Google\Cloud\Core\OptionsValidator())->stripUnknownOptions( + $options, + ['parameters', 'types'], + \Google\ApiCore\Options\CallOptions::class, + \Google\Cloud\Spanner\V1\ExecuteSqlRequest::class ); $executeSqlOptions['seqno'] = $this->seqno++; $executeSqlOptions['transaction'] = $txnOptions; @@ -275,9 +281,8 @@ public function execute(string $sql, array $options = []): Result $executeSqlOptions['requestOptions']['transactionTag'] = $this->tag; } - $result = $this->operation->execute($this->session, $sql, $executeSqlOptions + [ - 'route-to-leader' => $this->context === Database::CONTEXT_READWRITE - ]); + $executeSqlOptions['route-to-leader'] = $this->context === Database::CONTEXT_READWRITE; + $result = $this->operation->execute($this->session, $sql, $executeSqlOptions); if (empty($this->id()) && $result->transaction()) { $this->setId($result->transaction()->id()); @@ -332,6 +337,10 @@ public function execute(string $sql, array $options = []): Result * and not Snapshots. * {@see \Google\Cloud\Spanner\V1\ReadRequest} and {@see \Google\Cloud\Spanner\V1\ReadRequest\LockHint} * for more information and available options. + * @type array $headers Headers to be set with the request. + * @type array|RetrySettings $retrySettings Retry settings to be used with the request. + * @type int $timeoutMillis Timeout to use for this call. + * @type array $transportOptions Options to be used for the transport. * } * @return Result */ @@ -340,9 +349,10 @@ public function read(string $table, KeySet $keySet, array $columns, array $optio $this->singleUseState(); $this->checkReadContext(); - $readOptions = $this->pluckArray( - ['index', 'limit', 'partitionToken', 'requestOptions', 'directedReadOptions'], + $readOptions = (new \Google\Cloud\Core\OptionsValidator())->stripUnknownOptions( $options, + \Google\ApiCore\Options\CallOptions::class, + \Google\Cloud\Spanner\V1\ReadRequest::class ); $options['transactionType'] = $this->context; if (empty($this->transactionId) && isset($this->transactionSelector['begin'])) { @@ -362,9 +372,8 @@ public function read(string $table, KeySet $keySet, array $columns, array $optio $readOptions, $this->directedReadOptions ?? [] ); - $result = $this->operation->read($this->session, $table, $keySet, $columns, $readOptions + [ - 'route-to-leader' => $this->context === Database::CONTEXT_READWRITE - ]); + $readOptions['route-to-leader'] = $this->context === Database::CONTEXT_READWRITE; + $result = $this->operation->read($this->session, $table, $keySet, $columns, $readOptions); if (empty($this->id()) && $result->transaction()) { $this->setId($result->transaction()->id()); diff --git a/Spanner/src/ValueMapper.php b/Spanner/src/ValueMapper.php index 93344374bbec..e4cceff45bd6 100644 --- a/Spanner/src/ValueMapper.php +++ b/Spanner/src/ValueMapper.php @@ -561,7 +561,7 @@ private function structParam(StructValue|array|null $value, StructType $type): a } } elseif ($value instanceof StructValue) { foreach ($value->values() as $idx => $val) { - $name = $val['name']; + $name = (string) $val['name']; $valValue = $val['value']; if (!isset($values[$name])) { @@ -580,7 +580,7 @@ private function structParam(StructValue|array|null $value, StructType $type): a $fields = []; $names = []; foreach ($typeFields as $typeIndex => $paramType) { - $fieldName = $paramType['name']; + $fieldName = (string) $paramType['name']; // Count the number of times the field name has been encountered thus far. // This lets us pick the correct index for duplicate fields. diff --git a/Spanner/tests/Unit/DatabaseTest.php b/Spanner/tests/Unit/DatabaseTest.php index 7c2419fe9c05..730ab9ed5d66 100644 --- a/Spanner/tests/Unit/DatabaseTest.php +++ b/Spanner/tests/Unit/DatabaseTest.php @@ -1069,6 +1069,36 @@ public function testInsertBatch() $this->assertTimestampIsCorrect($res); } + public function testInsertBatchWithOptions() + { + $table = 'foo'; + $row = ['col' => 'val']; + $options = [ + 'requestOptions' => ['priority' => 1], + 'headers' => ['custom-header' => 'value'], + 'retrySettings' => ['retriesEnabled' => false], + 'timeoutMillis' => 1234, + 'transportOptions' => ['grpc' => ['timeout' => 100]], + ]; + + $this->spannerClient->commit( + Argument::that(function ($request) { + return $request->getRequestOptions()->getPriority() === 1; + }), + Argument::that(function (array $callOptions) { + $this->assertEquals('value', $callOptions['headers']['custom-header']); + $this->assertEquals(['retriesEnabled' => false], $callOptions['retrySettings']); + $this->assertEquals(1234, $callOptions['timeoutMillis']); + $this->assertEquals(['grpc' => ['timeout' => 100]], $callOptions['transportOptions']); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn($this->commitResponse()); + + $this->database->insertBatch($table, [$row], $options); + } + public function testUpdate() { $table = 'foo'; diff --git a/Spanner/tests/Unit/OperationTest.php b/Spanner/tests/Unit/OperationTest.php index 692bd0a32ac0..1a2492476e91 100644 --- a/Spanner/tests/Unit/OperationTest.php +++ b/Spanner/tests/Unit/OperationTest.php @@ -647,6 +647,48 @@ public function testPartitionRead() $this->assertEquals($partitionToken2, $res[1]->token()); } + public function testPartitionReadWithOptions() + { + $transactionId = 'foo'; + $partitionToken1 = 'token1'; + $options = [ + 'headers' => ['custom-header' => 'value'], + 'retrySettings' => ['retriesEnabled' => false], + 'timeoutMillis' => 1234, + 'transportOptions' => ['grpc' => ['timeout' => 100]], + ]; + + $this->spannerClient->partitionRead( + Argument::any(), + Argument::that(function (array $callOptions) { + $this->assertEquals($callOptions['headers']['custom-header'], 'value'); + $this->assertEquals($callOptions['retrySettings'], ['retriesEnabled' => false]); + $this->assertEquals($callOptions['timeoutMillis'], 1234); + $this->assertEquals($callOptions['transportOptions'], ['grpc' => ['timeout' => 100]]); + return true; + }) + ) + ->shouldBeCalled() + ->willReturn(new PartitionResponse([ + 'partitions' => [ + new Partition(['partition_token' => $partitionToken1]), + ] + ])); + + $res = $this->operation->partitionRead( + $this->session, + $transactionId, + 'Posts', + new KeySet(['all' => true]), + ['foo'], + $options + ); + + $this->assertContainsOnlyInstancesOf(ReadPartition::class, $res); + $this->assertCount(1, $res); + $this->assertEquals($partitionToken1, $res[0]->token()); + } + public function testCommitWithPrecommitTokenOnRetry() { $failureResponse = (new CommitResponse()) diff --git a/Spanner/tests/Unit/TransactionTest.php b/Spanner/tests/Unit/TransactionTest.php index 20658bad9458..de43e1dfb61c 100644 --- a/Spanner/tests/Unit/TransactionTest.php +++ b/Spanner/tests/Unit/TransactionTest.php @@ -141,6 +141,38 @@ public function testExecute() $rows = iterator_to_array($res->rows()); $this->assertEquals(10, $rows[0]['ID']); } + public function testExecuteWithOptions() + { + $sql = 'SELECT * FROM Table'; + $options = [ + 'requestOptions' => ['priority' => 1], + 'headers' => ['custom-header' => 'value'], + 'retrySettings' => ['retriesEnabled' => false], + 'timeoutMillis' => 1234, + 'transportOptions' => ['grpc' => ['timeout' => 100]], + ]; + $this->spannerClient->executeStreamingSql( + Argument::that(function (ExecuteSqlRequest $request) use ($sql) { + $this->assertEquals($request->getTransaction()->getId(), self::TRANSACTION); + $this->assertEquals($request->getSql(), $sql); + $this->assertEquals($request->getRequestOptions()->getPriority(), 1); + return true; + }), + Argument::that(function (array $callOptions) { + $this->assertEquals($callOptions['route-to-leader'], true); + $this->assertEquals($callOptions['headers']['custom-header'], 'value'); + $this->assertEquals($callOptions['retrySettings'], ['retriesEnabled' => false]); + $this->assertEquals($callOptions['timeoutMillis'], 1234); + $this->assertEquals($callOptions['transportOptions'], ['grpc' => ['timeout' => 100]]); + return true; + }) + ) + ->shouldBeCalledOnce() + ->willReturn($this->resultGeneratorStream()); + + $res = $this->transaction->execute($sql, $options); + $this->assertInstanceOf(Result::class, $res); + } public function testExecuteUpdate() { @@ -474,6 +506,42 @@ public function testCommit() $transaction->commit(['requestOptions' => ['requestTag' => 'unused']]); } + public function testCommitWithOptions() + { + $options = [ + 'requestOptions' => ['priority' => 1], + 'headers' => ['custom-header' => 'value'], + 'retrySettings' => ['retriesEnabled' => false], + 'timeoutMillis' => 1234, + 'transportOptions' => ['grpc' => ['timeout' => 100]], + ]; + + $expectedOptions = $options; + $expectedOptions['requestOptions']['transactionTag'] = self::TRANSACTION_TAG; + $expectedOptions['transactionId'] = self::TRANSACTION; + + $operation = $this->prophesize(Operation::class); + $operation->commit( + $this->session->reveal(), + Argument::any(), + $expectedOptions + ) + ->shouldBeCalled() + ->willReturn($this->commitResponseWithCommitStats()); + + $transaction = new Transaction( + $operation->reveal(), + $this->session->reveal(), + self::TRANSACTION, + ['tag' => self::TRANSACTION_TAG] + ); + + $transaction->insert('Posts', ['foo' => 'bar']); + $optionsPassed = $options; + $optionsPassed['requestOptions']['requestTag'] = 'unused'; + $transaction->commit($optionsPassed); + } + public function testCommitWithReturnCommitStats() { $operation = $this->prophesize(Operation::class); diff --git a/Spanner/tests/Unit/ValueMapperTest.php b/Spanner/tests/Unit/ValueMapperTest.php index fe4247c42fd1..16cd00381320 100644 --- a/Spanner/tests/Unit/ValueMapperTest.php +++ b/Spanner/tests/Unit/ValueMapperTest.php @@ -1365,7 +1365,7 @@ private function createField($code, $typeAnnotationCode = null, $type = null, ar 'type' => array_filter([ 'code' => $code, 'typeAnnotation' => $typeAnnotationCode, - $type => $typeObj + (string) $type => $typeObj ]) ]]; }