diff --git a/Spanner/src/Connection/Grpc.php b/Spanner/src/Connection/Grpc.php index 004185c78daa..c9be4db37bbd 100644 --- a/Spanner/src/Connection/Grpc.php +++ b/Spanner/src/Connection/Grpc.php @@ -47,6 +47,7 @@ use Google\Cloud\Spanner\SpannerClient as ManualSpannerClient; use Google\Cloud\Spanner\V1\CreateSessionRequest; use Google\Cloud\Spanner\V1\DeleteSessionRequest; +use Google\Cloud\Spanner\V1\DirectedReadOptions; use Google\Cloud\Spanner\V1\ExecuteBatchDmlRequest\Statement; use Google\Cloud\Spanner\V1\ExecuteSqlRequest\QueryOptions; use Google\Cloud\Spanner\V1\KeySet; @@ -941,6 +942,7 @@ public function executeStreamingSql(array $args) $queryOptions += ['optimizerStatisticsPackage' => $envQueryOptimizerStatisticsPackage]; } $queryOptions += $this->defaultQueryOptions; + $this->setDirectedReadOptions($args); if ($queryOptions) { $args['queryOptions'] = $this->serializer->decodeMessage( @@ -980,7 +982,7 @@ public function streamingRead(array $args) $requestOptions ); } - + $this->setDirectedReadOptions($args); $args['transaction'] = $this->createTransactionSelector($args); $databaseName = $this->pluck('database', $args); @@ -1597,4 +1599,20 @@ private function getLroResponseMapper($typeUrl) return null; } + + /** + * Set DirectedReadOptions if provided. + * + * @param array $args + */ + private function setDirectedReadOptions(array &$args) + { + $directedReadOptions = $this->pluck('directedReadOptions', $args, false); + if (!empty($directedReadOptions)) { + $args['directedReadOptions'] = $this->serializer->decodeMessage( + new DirectedReadOptions, + $directedReadOptions + ); + } + } } diff --git a/Spanner/src/Database.php b/Spanner/src/Database.php index bad2a759ae86..2106a0f5d1d0 100644 --- a/Spanner/src/Database.php +++ b/Spanner/src/Database.php @@ -177,6 +177,11 @@ class Database */ private $databaseRole; + /** + * @var array + */ + private $directedReadOptions; + /** * Create an object representing a Database. * @@ -222,6 +227,7 @@ public function __construct( $this->setLroProperties($lroConnection, $lroCallables, $this->name); $this->databaseRole = $databaseRole; + $this->directedReadOptions = $instance->directedReadOptions(); } /** @@ -706,6 +712,10 @@ public function iam() * **Defaults to** `false`. * @type array $sessionOptions Session configuration and request options. * Session labels may be applied using the `labels` key. + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @return Snapshot * @throws \BadMethodCallException If attempting to call this method within @@ -723,6 +733,10 @@ public function snapshot(array $options = []) ]; $options['transactionOptions'] = $this->configureSnapshotOptions($options); + $options['directedReadOptions'] = $this->configureDirectedReadOptions( + $options, + $this->directedReadOptions ?? [] + ); $session = $this->selectSession( SessionPoolInterface::CONTEXT_READ, @@ -994,7 +1008,7 @@ public function runTransaction(callable $operation, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1043,7 +1057,7 @@ public function insert($table, array $data, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1089,7 +1103,7 @@ public function insertBatch($table, array $dataSet, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1135,7 +1149,7 @@ public function update($table, array $data, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1182,7 +1196,7 @@ public function updateBatch($table, array $dataSet, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1230,7 +1244,7 @@ public function insertOrUpdate($table, array $data, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1277,7 +1291,7 @@ public function insertOrUpdateBatch($table, array $dataSet, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1325,7 +1339,7 @@ public function replace($table, array $data, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1375,7 +1389,7 @@ public function replaceBatch($table, array $dataSet, array $options = []) * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use @@ -1629,12 +1643,16 @@ public function delete($table, KeySet $keySet, array $options = []) * optimizer version will fail with a syntax error * (`INVALID_ARGUMENT`) status. * @type array $requestOptions Request options. - * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). - * 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. + * For more information on available options, please see + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * 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 $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @codingStandardsIgnoreEnd * @return Result @@ -1653,6 +1671,11 @@ public function execute($sql, array $options = []) $options['transactionContext'] ) = $this->transactionSelector($options); + $options['directedReadOptions'] = $this->configureDirectedReadOptions( + $options, + $this->directedReadOptions ?? [] + ); + try { return $this->operation->execute($session, $sql, $options); } finally { @@ -1769,11 +1792,11 @@ public function execute($sql, array $options = []) * parameter types. Likewise, for structs, use * {@see \Google\Cloud\Spanner\StructType}. * @type array $requestOptions Request options. - * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). - * 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 partitioned DML. + * For more information on available options, please see + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * 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 partitioned DML. * } * @return int The number of rows modified. */ @@ -1901,11 +1924,15 @@ public function executePartitionedUpdate($statement, array $options = []) * @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 - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). - * 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. + * For more information on available options, please see + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * 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 $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @codingStandardsIgnoreEnd * @return Result @@ -1922,6 +1949,11 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] $options['transaction'] = $transactionOptions; $options['transactionContext'] = $context; + $options['directedReadOptions'] = $this->configureDirectedReadOptions( + $options, + $this->directedReadOptions ?? [] + ); + try { return $this->operation->read($session, $table, $keySet, $columns, $options); } finally { @@ -2100,7 +2132,7 @@ private function selectSession($context = SessionPoolInterface::CONTEXT_READ, ar * * @type array $requestOptions Request options. * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). * 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 single-use transactions. diff --git a/Spanner/src/Instance.php b/Spanner/src/Instance.php index f7d158d7a567..2144a6fde12f 100644 --- a/Spanner/src/Instance.php +++ b/Spanner/src/Instance.php @@ -122,6 +122,11 @@ class Instance */ private $iam; + /** + * @var array + */ + private $directedReadOptions; + /** * Create an object representing a Cloud Spanner instance. * @@ -137,6 +142,14 @@ class Instance * returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit platform * compatibility. **Defaults to** false. * @param array $info [optional] A representation of the instance object. + * @param array $options [optional]{ + * Instance options + * + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. + * } */ public function __construct( ConnectionInterface $connection, @@ -145,7 +158,8 @@ public function __construct( $projectId, $name, $returnInt64AsObject = false, - array $info = [] + array $info = [], + array $options = [] ) { $this->connection = $connection; $this->projectId = $projectId; @@ -154,6 +168,7 @@ public function __construct( $this->info = $info; $this->setLroProperties($lroConnection, $lroCallables, $this->name); + $this->directedReadOptions = $options['directedReadOptions'] ?? []; } /** @@ -794,4 +809,19 @@ public function __debugInfo() 'info' => $this->info ]; } + + /** + * Return the directed read options. + * + * Example: + * ``` + * $name = $instance->directedReadOptions(); + * ``` + * + * @return array + */ + public function directedReadOptions() + { + return $this->directedReadOptions; + } } diff --git a/Spanner/src/Operation.php b/Spanner/src/Operation.php index d466274b2241..8d34b96358dd 100644 --- a/Spanner/src/Operation.php +++ b/Spanner/src/Operation.php @@ -215,6 +215,10 @@ public function rollback(Session $session, $transactionId, array $options = []) * @type array $transaction Transaction selector options. * @type array $transaction.begin The begin transaction options. * [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions) + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @return Result */ @@ -414,6 +418,10 @@ public function executeUpdateBatch( * @type array $transaction Transaction selector options. * @type array $transaction.begin The begin transaction options. * [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions) + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @return Result */ @@ -559,6 +567,10 @@ public function createTransaction(Session $session, array $res = [], array $opti * @type string $className If set, an instance of the given class will * be instantiated. This setting is intended for internal use. * **Defaults to** `Google\Cloud\Spanner\Snapshot`. + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @return mixed */ diff --git a/Spanner/src/Snapshot.php b/Spanner/src/Snapshot.php index ae39afedebc7..a6d6081104f7 100644 --- a/Spanner/src/Snapshot.php +++ b/Spanner/src/Snapshot.php @@ -49,6 +49,10 @@ class Snapshot implements TransactionalReadInterface * @type string $id The Transaction ID. If no ID is provided, * the Transaction will be a Single-Use Transaction. * @type Timestamp $readTimestamp The read timestamp. + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @throws \InvalidArgumentException if a tag is specified as this is a read-only transaction. */ diff --git a/Spanner/src/SnapshotTrait.php b/Spanner/src/SnapshotTrait.php index 2cd9f7811fef..b01850b6f277 100644 --- a/Spanner/src/SnapshotTrait.php +++ b/Spanner/src/SnapshotTrait.php @@ -42,6 +42,10 @@ trait SnapshotTrait * @type string $id The Transaction ID. If no ID is provided, * the Transaction will be a Single-Use Transaction. * @type Timestamp $readTimestamp The read timestamp. + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } */ private function initialize( @@ -68,6 +72,7 @@ private function initialize( : self::TYPE_SINGLE_USE; $this->context = SessionPoolInterface::CONTEXT_READ; + $this->directedReadOptions = $options['directedReadOptions'] ?? []; $this->options = $options; } diff --git a/Spanner/src/SpannerClient.php b/Spanner/src/SpannerClient.php index a6a38312aa20..a1dedb0cf225 100644 --- a/Spanner/src/SpannerClient.php +++ b/Spanner/src/SpannerClient.php @@ -73,6 +73,26 @@ * $spanner = new SpannerClient(); * ``` * + * ``` + * use Google\Cloud\Spanner\SpannerClient; + * use Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type as ReplicaType; + * + * $directedOptions = [ + * 'directedReadOptions' => [ + * 'includeReplicas' => [ + * 'replicaSelections' => [ + * [ + * 'location' => 'us-central1', + * 'type' => ReplicaType::READ_WRITE + * ] + * ], + * 'autoFailoverDisabled' => false + * ] + * ] + * ]; + * $spanner = new SpannerClient($directedOptions); + * ``` + * * @method resumeOperation() { * Resume a Long Running Operation * @@ -109,6 +129,11 @@ class SpannerClient */ private $returnInt64AsObject; + /** + * @var array + */ + private $directedReadOptions; + /** * Create a Spanner client. Please note that this client requires * [the gRPC extension](https://cloud.google.com/php/grpc). @@ -164,6 +189,10 @@ class SpannerClient * (retry every failed request up to `retries` times). * `true`: use discrete backoff settings based on called method name. * **Defaults to** `false`. + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @throws GoogleException If the gRPC extension is not enabled. */ @@ -240,6 +269,8 @@ public function __construct(array $config = []) } ] ]); + + $this->directedReadOptions = $config['directedReadOptions'] ?? []; } /** @@ -518,7 +549,8 @@ public function instance($name, array $instance = []) $this->projectId, $name, $this->returnInt64AsObject, - $instance + $instance, + ['directedReadOptions' => $this->directedReadOptions] ); } diff --git a/Spanner/src/TransactionConfigurationTrait.php b/Spanner/src/TransactionConfigurationTrait.php index cee3bd2fa8b1..f3307152d478 100644 --- a/Spanner/src/TransactionConfigurationTrait.php +++ b/Spanner/src/TransactionConfigurationTrait.php @@ -63,6 +63,37 @@ private function transactionSelector(array &$options, array $previous = []) ]; } + /** + * Configure the DirectedReadOptions. + * + * Request level DirectedReadOptions takes precedence over client level DirectedReadOptions. + * Client level DirectedReadOptions apply only to read-only and single-use transactions. + * + * @param array $requestOptions Request level options. + * @param array $clientOptions Client level Directed Read Options. + * @return array + */ + private function configureDirectedReadOptions(array $requestOptions, array $clientOptions) + { + if (isset($requestOptions['directedReadOptions'])) { + return $requestOptions['directedReadOptions']; + } + + if (isset($requestOptions['transaction']['singleUse']) || ( + isset($requestOptions['transactionContext']) && + $requestOptions['transactionContext'] == SessionPoolInterface::CONTEXT_READ + ) || isset($requestOptions['transactionOptions']['readOnly']) + ) { + if (isset($clientOptions['includeReplicas'])) { + return ['includeReplicas' => $clientOptions['includeReplicas']]; + } elseif (isset($clientOptions['excludeReplicas'])) { + return ['excludeReplicas' => $clientOptions['excludeReplicas']]; + } + } + + return []; + } + /** * Return transaction options based on given configuration options. * diff --git a/Spanner/src/TransactionalReadTrait.php b/Spanner/src/TransactionalReadTrait.php index 230c5470f986..f2531128c6eb 100644 --- a/Spanner/src/TransactionalReadTrait.php +++ b/Spanner/src/TransactionalReadTrait.php @@ -72,6 +72,11 @@ trait TransactionalReadTrait */ private $tag = null; + /** + * @var array + */ + private $directedReadOptions = []; + /** * Run a query. * @@ -252,12 +257,16 @@ trait TransactionalReadTrait * optimizer version will fail with a syntax error * (`INVALID_ARGUMENT`) status. * @type array $requestOptions Request options. - * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). - * 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 the transaction tag should have already - * been set when creating the transaction. + * For more information on available options, please see + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * 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 the transaction tag should have already + * been set when creating the transaction. + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @codingStandardsIgnoreEnd * @return Result @@ -288,6 +297,11 @@ public function execute($sql, array $options = []) $options['requestOptions']['transactionTag'] = $this->tag; } + $options['directedReadOptions'] = $this->configureDirectedReadOptions( + $options, + $this->directedReadOptions ?? [] + ); + $result = $this->operation->execute($this->session, $sql, $options); if (empty($this->id()) && $result->transaction()) { $this->setId($result->transaction()->id()); @@ -324,12 +338,16 @@ public function execute($sql, array $options = []) * @type string $index The name of an index on the table. * @type int $limit The number of results to return. * @type array $requestOptions Request options. - * For more information on available options, please see - * [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). - * 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 the transaction tag should have already - * been set when creating the transaction. + * For more information on available options, please see + * [RequestOptions](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions). + * 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 the transaction tag should have already + * been set when creating the transaction. + * @type array $directedReadOptions Directed read options. + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions} + * If using the `replicaSelection::type` setting, utilize the constants available in + * {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value. * } * @return Result */ @@ -357,6 +375,11 @@ public function read($table, KeySet $keySet, array $columns, array $options = [] $options['requestOptions']['transactionTag'] = $this->tag; } + $options['directedReadOptions'] = $this->configureDirectedReadOptions( + $options, + $this->directedReadOptions ?? [] + ); + $result = $this->operation->read($this->session, $table, $keySet, $columns, $options); if (empty($this->id()) && $result->transaction()) { $this->setId($result->transaction()->id()); diff --git a/Spanner/tests/Snippet/ArrayTypeTest.php b/Spanner/tests/Snippet/ArrayTypeTest.php index aa34dc621bc2..db0b98de2eac 100644 --- a/Spanner/tests/Snippet/ArrayTypeTest.php +++ b/Spanner/tests/Snippet/ArrayTypeTest.php @@ -58,6 +58,7 @@ public function setUp(): void $instance = $this->prophesize(Instance::class); $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); + $instance->directedReadOptions()->willReturn([]); $session = $this->prophesize(Session::class); $session->info() diff --git a/Spanner/tests/Snippet/BatchDmlResultTest.php b/Spanner/tests/Snippet/BatchDmlResultTest.php index f74a6a8285dc..f6945b8ca479 100644 --- a/Spanner/tests/Snippet/BatchDmlResultTest.php +++ b/Spanner/tests/Snippet/BatchDmlResultTest.php @@ -106,6 +106,7 @@ public function testClass() $instance = $this->prophesize(Instance::class); $instance->name()->willReturn('projects/test-project/instances/my-instance'); + $instance->directedReadOptions()->willReturn([]); $database = TestHelpers::stub(Database::class, [ $connection->reveal(), diff --git a/Spanner/tests/Snippet/SpannerClientTest.php b/Spanner/tests/Snippet/SpannerClientTest.php index e71ba05eaa92..26a93cb09293 100644 --- a/Spanner/tests/Snippet/SpannerClientTest.php +++ b/Spanner/tests/Snippet/SpannerClientTest.php @@ -298,4 +298,11 @@ public function testBatchWithDatabaseRole() $res = $snippet->invoke('batch'); $this->assertInstanceOf(BatchClient::class, $res->returnVal()); } + + public function testClassWithDirectedRead() + { + $snippet = $this->snippetFromClass(SpannerClient::class, 2); + $res = $snippet->invoke('spanner'); + $this->assertInstanceOf(SpannerClient::class, $res->returnVal()); + } } diff --git a/Spanner/tests/Snippet/StructTypeTest.php b/Spanner/tests/Snippet/StructTypeTest.php index 425db1d45701..bbd1f7bc08ef 100644 --- a/Spanner/tests/Snippet/StructTypeTest.php +++ b/Spanner/tests/Snippet/StructTypeTest.php @@ -58,6 +58,7 @@ public function setUp(): void $instance = $this->prophesize(Instance::class); $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); + $instance->directedReadOptions()->willReturn([]); $session = $this->prophesize(Session::class); $session->info() diff --git a/Spanner/tests/Snippet/StructValueTest.php b/Spanner/tests/Snippet/StructValueTest.php index 33d3b5fd256f..64eab73f2f23 100644 --- a/Spanner/tests/Snippet/StructValueTest.php +++ b/Spanner/tests/Snippet/StructValueTest.php @@ -57,6 +57,7 @@ public function setUp(): void $instance = $this->prophesize(Instance::class); $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); + $instance->directedReadOptions()->willReturn([]); $session = $this->prophesize(Session::class); $session->info() diff --git a/Spanner/tests/System/TransactionTest.php b/Spanner/tests/System/TransactionTest.php index 59ac6c787efe..d3ec2e08cfb7 100644 --- a/Spanner/tests/System/TransactionTest.php +++ b/Spanner/tests/System/TransactionTest.php @@ -21,6 +21,7 @@ use Google\Cloud\Spanner\KeySet; use Google\Cloud\Core\Exception\ServiceException; use Google\Cloud\Spanner\Timestamp; +use Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type as ReplicaType; /** * @group spanner @@ -277,6 +278,111 @@ public function testRunTransactionWithDbRole($db, $values, $expected) } } + /** + * @dataProvider getDirectedReadOptions + */ + public function testTransactionExecuteWithDirectedRead($directedReadOptions) + { + // Emulator does not support DirectedRead + $this->skipEmulatorTests(); + + $db = self::$database; + $id = $this->randId(); + + $db->insert(self::$tableName, [ + 'id' => $id, + 'number' => 0 + ]); + + $snapshot = $db->snapshot(); + $rows = $snapshot->execute( + 'SELECT * FROM ' . self::$tableName . ' WHERE id = ' . $id, + $directedReadOptions + )->rows()->current(); + $this->assertEquals(0, $rows['number']); + + $rows = $db->execute( + 'SELECT * FROM ' . self::$tableName . ' WHERE id = ' . $id, + ['transactionId' => $snapshot->id()] + $directedReadOptions + )->rows()->current(); + $this->assertEquals(0, $rows['number']); + } + + /** + * @dataProvider getDirectedReadOptions + */ + public function testRWTransactionExecuteFailsWithDirectedRead($directedReadOptions) + { + // Emulator does not support DirectedRead + $this->skipEmulatorTests(); + + $db = self::$database; + $transaction = $db->transaction(); + $expected = 'Directed reads can only be performed in a read-only transaction.'; + $exception = null; + + try { + $rows = $db->execute( + 'SELECT * FROM ' . self::$tableName, + ['transactionId' => $transaction->id()] + $directedReadOptions + )->rows()->current(); + } catch (ServiceException $e) { + $exception = $e; + } + $this->assertEquals($exception->getServiceException()->getBasicMessage(), $expected); + + $exception = null; + try { + $row = $transaction->execute( + 'SELECT * FROM ' . self::$tableName, + $directedReadOptions + )->rows()->current(); + } catch (ServiceException $e) { + $exception = $e; + } + $this->assertEquals($exception->getServiceException()->getBasicMessage(), $expected); + } + + /** + * @dataProvider getDirectedReadOptions + */ + public function testRWTransactionReadFailsWithDirectedRead($directedReadOptions) + { + // Emulator does not support DirectedRead + $this->skipEmulatorTests(); + + $db = self::$database; + $transaction = $db->transaction(); + $expected = 'Directed reads can only be performed in a read-only transaction.'; + $exception = null; + + list($keySet, $cols) = $this->readArgs(); + try { + $res = $db->read( + self::TEST_TABLE_NAME, + $keySet, + $cols, + ['transactionId' => $transaction->id()] + $directedReadOptions + )->rows()->current(); + } catch (ServiceException $e) { + $exception = $e; + } + $this->assertEquals($exception->getServiceException()->getBasicMessage(), $expected); + $exception = null; + + try { + $res = $transaction->read( + self::TEST_TABLE_NAME, + $keySet, + $cols, + $directedReadOptions + )->rows()->current(); + } catch (ServiceException $e) { + $exception = $e; + } + $this->assertEquals($exception->getServiceException()->getBasicMessage(), $expected); + } + public function testRunTransactionILBWithMultipleOperations() { $db = self::$database; @@ -341,6 +447,38 @@ public function testRunTransactionILBWithMultipleOperations() $this->assertEquals([1], $res->rowCounts()); } + public function getDirectedReadOptions() + { + return + [ + [[ + 'directedReadOptions' => [ + 'includeReplicas' => [ + 'replicaSelections' => [ + [ + 'location' => 'asia-northeast1', + 'type' => ReplicaType::READ_WRITE + ] + ], + 'autoFailoverDisabled' => false + ] + ] + ]], + [[ + 'directedReadOptions' => [ + 'excludeReplicas' => [ + 'replicaSelections' => [ + [ + 'location' => 'asia-northeast1', + 'type' => ReplicaType::READ_WRITE + ] + ] + ] + ] + ]] + ]; + } + private function readArgs() { return [ diff --git a/Spanner/tests/Unit/DatabaseTest.php b/Spanner/tests/Unit/DatabaseTest.php index 6aa121911d5c..47ecfe68e345 100644 --- a/Spanner/tests/Unit/DatabaseTest.php +++ b/Spanner/tests/Unit/DatabaseTest.php @@ -81,6 +81,8 @@ class DatabaseTest extends TestCase private $database; private $session; private $databaseWithDatabaseRole; + private $directedReadOptionsIncludeReplicas; + private $directedReadOptionsExcludeReplicas; public function setUp(): void @@ -99,13 +101,34 @@ public function setUp(): void self::DATABASE, self::SESSION ]); + $this->directedReadOptionsIncludeReplicas = [ + 'includeReplicas' => [ + 'replicaSelections' => [ + 'location' => 'us-central1', + 'type' => 'READ_WRITE', + 'autoFailoverDisabled' => false + ] + ] + ]; + $this->directedReadOptionsExcludeReplicas = [ + 'excludeReplicas' => [ + 'replicaSelections' => [ + 'location' => 'us-central1', + 'type' => 'READ_WRITE', + 'autoFailoverDisabled' => false + ] + ] + ]; $this->instance = TestHelpers::stub(Instance::class, [ $this->connection->reveal(), $this->lro->reveal(), $this->lroCallables, self::PROJECT, - self::INSTANCE + self::INSTANCE, + false, + [], + ['directedReadOptions' => $this->directedReadOptionsIncludeReplicas] ], [ 'info', 'connection' @@ -1407,6 +1430,86 @@ public function testDBDatabaseRole() $this->databaseWithDatabaseRole->execute($sql); } + public function testExecuteWithDirectedRead() + { + $this->connection->executeStreamingSql(Argument::withEntry( + 'directedReadOptions', + $this->directedReadOptionsIncludeReplicas + )) + ->shouldBeCalled() + ->willReturn($this->resultGenerator()); + + $sql = 'SELECT * FROM Table'; + $res = $this->database->execute($sql); + $this->assertInstanceOf(Result::class, $res); + $rows = iterator_to_array($res->rows()); + $this->assertCount(1, $rows); + } + + public function testPrioritizeExecuteDirectedReadOptions() + { + $this->connection->executeStreamingSql(Argument::withEntry( + 'directedReadOptions', + $this->directedReadOptionsExcludeReplicas + )) + ->shouldBeCalled() + ->willReturn($this->resultGenerator()); + + $sql = 'SELECT * FROM Table'; + $res = $this->database->execute( + $sql, + ['directedReadOptions' => $this->directedReadOptionsExcludeReplicas] + ); + $this->assertInstanceOf(Result::class, $res); + $rows = iterator_to_array($res->rows()); + $this->assertCount(1, $rows); + } + + public function testReadWithDirectedRead() + { + $table = 'foo'; + $keys = [10, 'bar']; + $columns = ['id', 'name']; + $this->connection->streamingRead(Argument::withEntry( + 'directedReadOptions', + $this->directedReadOptionsIncludeReplicas + )) + ->shouldBeCalled() + ->willReturn($this->resultGenerator()); + + $res = $this->database->read( + $table, + new KeySet(['keys' => $keys]), + $columns + ); + $this->assertInstanceOf(Result::class, $res); + $rows = iterator_to_array($res->rows()); + $this->assertCount(1, $rows); + } + + public function testPrioritizeReadDirectedReadOptions() + { + $table = 'foo'; + $keys = [10, 'bar']; + $columns = ['id', 'name']; + $this->connection->streamingRead(Argument::withEntry( + 'directedReadOptions', + $this->directedReadOptionsExcludeReplicas + )) + ->shouldBeCalled() + ->willReturn($this->resultGenerator()); + + $res = $this->database->read( + $table, + new KeySet(['keys' => $keys]), + $columns, + ['directedReadOptions' => $this->directedReadOptionsExcludeReplicas] + ); + $this->assertInstanceOf(Result::class, $res); + $rows = iterator_to_array($res->rows()); + $this->assertCount(1, $rows); + } + public function testRunTransactionWithUpdate() { $sql = $this->createStreamingAPIArgs()['sql']; diff --git a/Spanner/tests/Unit/InstanceTest.php b/Spanner/tests/Unit/InstanceTest.php index fb54a18f1d53..cb0564d92e0e 100644 --- a/Spanner/tests/Unit/InstanceTest.php +++ b/Spanner/tests/Unit/InstanceTest.php @@ -34,6 +34,8 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Google\Cloud\Spanner\KeySet; +use Google\Cloud\Spanner\Result; use Google\Cloud\Spanner\Tests\ResultGeneratorTrait; /** @@ -46,6 +48,7 @@ class InstanceTest extends TestCase use ProphecyTrait; use ResultGeneratorTrait; use StubCreationTrait; + use ResultGeneratorTrait; const PROJECT_ID = 'test-project'; const NAME = 'instance-name'; @@ -56,18 +59,31 @@ class InstanceTest extends TestCase private $connection; private $instance; private $lroConnection; + private $directedReadOptionsIncludeReplicas; public function setUp(): void { $this->checkAndSkipGrpcTests(); $this->connection = $this->getConnStub(); + $this->directedReadOptionsIncludeReplicas = [ + 'includeReplicas' => [ + 'replicaSelections' => [ + 'location' => 'us-central1', + 'type' => 'READ_WRITE', + 'autoFailoverDisabled' => false + ] + ] + ]; $this->instance = TestHelpers::stub(Instance::class, [ $this->connection->reveal(), $this->prophesize(LongRunningConnectionInterface::class)->reveal(), [], self::PROJECT_ID, - self::NAME + self::NAME, + false, + [], + ['directedReadOptions' => $this->directedReadOptionsIncludeReplicas] ], [ 'info', 'connection' @@ -587,6 +603,66 @@ public function testInstanceDatabaseRole() $database->execute($sql); } + public function testInstanceExecuteWithDirectedRead() + { + $database = $this->instance->database( + $this::DATABASE + ); + $this->connection->createSession(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => self::SESSION + ]); + + $this->connection->executeStreamingSql(Argument::withEntry( + 'directedReadOptions', + $this->directedReadOptionsIncludeReplicas + )) + ->shouldBeCalled() + ->willReturn( + $this->resultGenerator() + ); + + $sql = 'SELECT * FROM Table'; + $res = $database->execute($sql); + $this->assertInstanceOf(Result::class, $res); + $rows = iterator_to_array($res->rows()); + $this->assertCount(1, $rows); + } + + public function testInstanceReadWithDirectedRead() + { + $table = 'foo'; + $keys = [10, 'bar']; + $columns = ['id', 'name']; + $database = $this->instance->database( + $this::DATABASE, + ); + $this->connection->createSession(Argument::any()) + ->shouldBeCalled() + ->willReturn([ + 'name' => self::SESSION + ]); + + $this->connection->streamingRead(Argument::withEntry( + 'directedReadOptions', + $this->directedReadOptionsIncludeReplicas + )) + ->shouldBeCalled() + ->willReturn( + $this->resultGenerator() + ); + + $res = $database->read( + $table, + new KeySet(['keys' => $keys]), + $columns + ); + $this->assertInstanceOf(Result::class, $res); + $rows = iterator_to_array($res->rows()); + $this->assertCount(1, $rows); + } + // ************** // private function getDefaultInstance() diff --git a/Spanner/tests/Unit/SnapshotTest.php b/Spanner/tests/Unit/SnapshotTest.php index 698709a4b607..b9469d900e2f 100644 --- a/Spanner/tests/Unit/SnapshotTest.php +++ b/Spanner/tests/Unit/SnapshotTest.php @@ -26,6 +26,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Google\Cloud\Spanner\KeySet; use Google\Cloud\Spanner\Result; /** @@ -38,6 +39,7 @@ class SnapshotTest extends TestCase private $timestamp; private $snapshot; + private $directedReadOptionsIncludeReplicas; public function setUp(): void { @@ -55,6 +57,13 @@ public function setUp(): void $this->prophesize(Session::class)->reveal(), $args ); + $this->directedReadOptionsIncludeReplicas = [ + 'includeReplicas' => [ + 'replicaSelections' => [ + 'location' => 'us-central1' + ] + ] + ]; } public function testTypeIsPreAllocated() @@ -110,4 +119,45 @@ public function testSingleUseFailsOnSecondUse() $snapshot->execute('foo'); $snapshot->execute('foo'); } + + public function testExecuteDirectedReadOptions() + { + $operation = $this->prophesize(Operation::class); + $result = $this->prophesize(Result::class); + $operation->execute(Argument::any(), Argument::any(), Argument::withEntry( + 'directedReadOptions', + $this->directedReadOptionsIncludeReplicas + ))->shouldBeCalled()->willReturn($result); + + $snapshot = new Snapshot( + $operation->reveal(), + $this->prophesize(Session::class)->reveal(), + ['directedReadOptions' => $this->directedReadOptionsIncludeReplicas] + ); + + $snapshot->execute('foo'); + } + + public function testReadDirectedReadOptions() + { + $keySet = new KeySet([ + 'keys' => [1337] + ]); + $columns = ['ID', 'title', 'content']; + + $operation = $this->prophesize(Operation::class); + $result = $this->prophesize(Result::class); + $operation->read(Argument::any(), Argument::any(), Argument::any(), Argument::any(), Argument::withEntry( + 'directedReadOptions', + $this->directedReadOptionsIncludeReplicas + ))->shouldBeCalled()->willReturn($result); + + $snapshot = new Snapshot( + $operation->reveal(), + $this->prophesize(Session::class)->reveal(), + ['directedReadOptions' => $this->directedReadOptionsIncludeReplicas] + ); + + $snapshot->read('foo', $keySet, $columns); + } } diff --git a/Spanner/tests/Unit/SpannerClientTest.php b/Spanner/tests/Unit/SpannerClientTest.php index 2fc996f4e87f..3f93170d22f1 100644 --- a/Spanner/tests/Unit/SpannerClientTest.php +++ b/Spanner/tests/Unit/SpannerClientTest.php @@ -61,14 +61,27 @@ class SpannerClientTest extends TestCase private $client; private $connection; + private $directedReadOptionsIncludeReplicas; public function setUp(): void { $this->checkAndSkipGrpcTests(); $this->connection = $this->getConnStub(); + $this->directedReadOptionsIncludeReplicas = [ + 'includeReplicas' => [ + 'replicaSelections' => [ + 'location' => 'us-central1', + 'type' => 'READ_WRITE', + 'autoFailoverDisabled' => false + ] + ] + ]; $this->client = TestHelpers::stub(SpannerClient::class, [ - ['projectId' => self::PROJECT] + [ + 'projectId' => self::PROJECT, + 'directedReadOptions' => $this->directedReadOptionsIncludeReplicas + ] ]); } @@ -444,4 +457,13 @@ public function testSpannerClientDatabaseRole() $instance->database(Argument::any(), ['databaseRole' => 'Reader'])->shouldBeCalled(); $this->client->connect($instance->reveal(), self::DATABASE, ['databaseRole' => 'Reader']); } + + public function testSpannerClientWithDirectedRead() + { + $instance = $this->client->instance('testInstance'); + $this->assertEquals( + $instance->directedReadOptions(), + $this->directedReadOptionsIncludeReplicas + ); + } } diff --git a/Spanner/tests/Unit/TransactionConfigurationTraitTest.php b/Spanner/tests/Unit/TransactionConfigurationTraitTest.php index 63be1187d1f5..527d42706c98 100644 --- a/Spanner/tests/Unit/TransactionConfigurationTraitTest.php +++ b/Spanner/tests/Unit/TransactionConfigurationTraitTest.php @@ -40,6 +40,7 @@ class TransactionConfigurationTraitTest extends TestCase private $ts; private $duration; private $dur = []; + private $directedReadOptionsIncludeReplicas; public function setUp(): void { @@ -48,6 +49,13 @@ public function setUp(): void $this->impl = new TransactionConfigurationTraitImplementation; $this->duration = new Duration(10, 1); $this->dur = ['seconds' => 10, 'nanos' => 1]; + $this->directedReadOptionsIncludeReplicas = [ + 'includeReplicas' => [ + 'replicaSelections' => [ + 'location' => 'us-central1' + ] + ] + ]; } public function testTransactionSelectorBasicSnapshot() @@ -181,6 +189,42 @@ public function testConfigureSnapshotOptionsInvalidReadTimestamp() $this->impl->proxyConfigureSnapshotOptions($args); } + public function testRequestLevelConfigureDirectedReadOptions() + { + $requestOptions = [ + 'transaction' => ['singleUse' => true], + 'directedReadOptions' => $this->directedReadOptionsIncludeReplicas + ]; + $clientOptions = []; + $res = $this->impl->proxyConfigureDirectedReadOptions($requestOptions, $clientOptions); + $this->assertEquals($res, $requestOptions['directedReadOptions']); + } + + public function testClientLevelConfigureDirectedReadOptions() + { + $requestOptions = ['transaction' => ['singleUse' => true]]; + $clientOptions = $this->directedReadOptionsIncludeReplicas; + $res = $this->impl->proxyConfigureDirectedReadOptions($requestOptions, $clientOptions); + $this->assertEquals($res, $clientOptions); + } + + public function testPrioritizeRequestLevelConfigureDirectedReadOptions() + { + $requestOptions = [ + 'transaction' => ['singleUse' => true], + 'directedReadOptions' => $this->directedReadOptionsIncludeReplicas + ]; + $clientOptions = [ + 'includeReplicas' => [ + 'replicaSelections' => [ + 'location' => 'us-central2' + ] + ] + ]; + $res = $this->impl->proxyConfigureDirectedReadOptions($requestOptions, $clientOptions); + $this->assertEquals($res, $requestOptions['directedReadOptions']); + } + public function timestamps() { return [ @@ -209,5 +253,10 @@ public function proxyConfigureSnapshotOptions(array &$options) { return $this->configureSnapshotOptions($options); } + + public function proxyConfigureDirectedReadOptions(array $args1, array $args2) + { + return $this->configureDirectedReadOptions($args1, $args2); + } } //@codingStandardsIgnoreEnd diff --git a/Spanner/tests/Unit/TransactionTypeTest.php b/Spanner/tests/Unit/TransactionTypeTest.php index e88ae36dcca6..d228794c863b 100644 --- a/Spanner/tests/Unit/TransactionTypeTest.php +++ b/Spanner/tests/Unit/TransactionTypeTest.php @@ -777,6 +777,7 @@ private function database(ConnectionInterface $connection) $operation = new Operation($connection, false); $instance = $this->prophesize(Instance::class); $instance->name()->willReturn(InstanceAdminClient::instanceName(self::PROJECT, self::INSTANCE)); + $instance->directedReadOptions()->willReturn([]); $database = TestHelpers::stub(Database::class, [ $connection,