Skip to content

Commit eb0de93

Browse files
feat: Add support for the Isolation Level value (#8234)
1 parent 914bab8 commit eb0de93

14 files changed

+415
-20
lines changed

Spanner/src/Connection/Grpc.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1117,6 +1117,10 @@ public function beginTransaction(array $args)
11171117
$args = $this->addLarHeader($args, $this->larEnabled);
11181118
}
11191119

1120+
if (isset($transactionOptions['isolationLevel'])) {
1121+
$options->setIsolationLevel($transactionOptions['isolationLevel']);
1122+
}
1123+
11201124
// NOTE: if set for read-only actions, will throw exception
11211125
if (isset($transactionOptions['excludeTxnFromChangeStreams'])) {
11221126
$options->setExcludeTxnFromChangeStreams(

Spanner/src/Database.php

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
use Google\Cloud\Spanner\Session\Session;
3838
use Google\Cloud\Spanner\Session\SessionPoolInterface;
3939
use Google\Cloud\Spanner\V1\SpannerClient as GapicSpannerClient;
40+
use Google\Cloud\Spanner\V1\TransactionOptions;
41+
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
4042
use Google\Cloud\Spanner\V1\TypeCode;
4143
use Google\Rpc\Code;
4244

@@ -192,6 +194,11 @@ class Database
192194
*/
193195
private $returnInt64AsObject;
194196

197+
/**
198+
* @var int
199+
*/
200+
private int $isolationLevel;
201+
195202
/**
196203
* Create an object representing a Database.
197204
*
@@ -210,6 +217,8 @@ class Database
210217
* be returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit
211218
* platform compatibility. **Defaults to** false.
212219
* @param string $databaseRole The user created database role which creates the session.
220+
* @param int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
221+
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
213222
*/
214223
public function __construct(
215224
ConnectionInterface $connection,
@@ -221,7 +230,8 @@ public function __construct(
221230
?SessionPoolInterface $sessionPool = null,
222231
$returnInt64AsObject = false,
223232
array $info = [],
224-
$databaseRole = ''
233+
$databaseRole = '',
234+
$isolationLevel = IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
225235
) {
226236
$this->connection = $connection;
227237
$this->instance = $instance;
@@ -239,6 +249,7 @@ public function __construct(
239249
$this->databaseRole = $databaseRole;
240250
$this->directedReadOptions = $instance->directedReadOptions();
241251
$this->returnInt64AsObject = $returnInt64AsObject;
252+
$this->isolationLevel = $isolationLevel;
242253
}
243254

244255
/**
@@ -799,6 +810,8 @@ public function snapshot(array $options = [])
799810
* Session labels may be applied using the `labels` key.
800811
* @type string $tag A transaction tag. Requests made using this transaction will
801812
* use this as the transaction tag.
813+
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
814+
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
802815
* }
803816
* @return Transaction
804817
* @throws \BadMethodCallException If attempting to call this method within
@@ -811,7 +824,9 @@ public function transaction(array $options = [])
811824
}
812825

813826
// Configure readWrite options here. Any nested options for readWrite should be added to this call
814-
$options['transactionOptions'] = $this->configureTransactionOptions($options['transactionOptions'] ?? []);
827+
$options['transactionOptions'] = $this->configureTransactionOptions($options['transactionOptions'] ?? [
828+
'isolationLevel' => $options['isolationLevel'] ?? $this->isolationLevel
829+
]);
815830

816831
$session = $this->selectSession(
817832
SessionPoolInterface::CONTEXT_READWRITE,
@@ -900,6 +915,9 @@ public function transaction(array $options = [])
900915
* Session labels may be applied using the `labels` key.
901916
* @type string $tag A transaction tag. Requests made using this transaction will
902917
* use this as the transaction tag.
918+
* @type array transactionOptions Options for the transaction.
919+
* {@see \Google\Cloud\Spanner\V1\TransactionOptions}
920+
* for available options
903921
* }
904922
* @return mixed The return value of `$operation`.
905923
* @throws \RuntimeException If a transaction is not committed or rolled back.
@@ -916,7 +934,11 @@ public function runTransaction(callable $operation, array $options = [])
916934
'maxRetries' => self::MAX_RETRIES,
917935
];
918936

919-
$options['transactionOptions'] = $this->configureTransactionOptions($options['transactionOptions'] ?? []);
937+
$transactionOptions = (isset($options['transactionOptions'])) ? $options['transactionOptions'] : [];
938+
939+
$options['transactionOptions'] = $this->configureTransactionOptions([
940+
'isolationLevel' => $options['transactionOptions']['isolationLevel'] ?? $this->isolationLevel
941+
] + $transactionOptions);
920942

921943
$session = $this->selectSession(
922944
SessionPoolInterface::CONTEXT_READWRITE,
@@ -928,6 +950,10 @@ public function runTransaction(callable $operation, array $options = [])
928950

929951
// Initial attempt requires to set `begin` options (ILB).
930952
if ($attempt === 0) {
953+
if (!isset($options['transactionOptions']['isolationLevel'])) {
954+
$options['transactionOptions']['isolationLevel'] = IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED;
955+
}
956+
931957
// Partitioned DML does not support ILB.
932958
if (!isset($options['transactionOptions']['partitionedDml'])) {
933959
$options['begin'] = $options['transactionOptions'];
@@ -1626,11 +1652,12 @@ public function delete($table, KeySet $keySet, array $options = [])
16261652
* timestamp.
16271653
* @type Duration $exactStaleness Represents a number of seconds. Executes
16281654
* all reads at a timestamp that is $exactStaleness old.
1629-
* @type bool $begin If true, will begin a new transaction. If a
1655+
* @type bool|array $begin If true, will begin a new transaction. If a
16301656
* read/write transaction is desired, set the value of
16311657
* $transactionType. If a transaction or snapshot is created, it
16321658
* will be returned as `$result->transaction()` or
16331659
* `$result->snapshot()`. **Defaults to** `false`.
1660+
* If $begin is an array {@see TransactionOptions}
16341661
* @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ`
16351662
* or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is
16361663
* chosen, any snapshot options will be disregarded. If `$begin`
@@ -1681,6 +1708,10 @@ public function execute($sql, array $options = [])
16811708
) = $this->transactionSelector($options);
16821709
$options = $this->addLarHeader($options, true, $options['transactionContext']);
16831710

1711+
if (isset($options['transaction']['readWrite'])) {
1712+
$options['transaction']['begin']['isolationLevel'] ??= $this->isolationLevel;
1713+
}
1714+
16841715
$options['directedReadOptions'] = $this->configureDirectedReadOptions(
16851716
$options,
16861717
$this->directedReadOptions ?? []
@@ -1745,7 +1776,7 @@ public function mutationGroup()
17451776
* transactions.
17461777
* }
17471778
*
1748-
* @retur \Generator {@see \Google\Cloud\Spanner\V1\BatchWriteResponse}
1779+
* @return \Generator {@see \Google\Cloud\Spanner\V1\BatchWriteResponse}
17491780
*
17501781
* @throws ApiException if the remote call fails
17511782
*/
@@ -1889,23 +1920,33 @@ public function batchWrite(array $mutationGroups, array $options = [])
18891920
* Please note, if using the `priority` setting you may utilize the constants available
18901921
* on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value.
18911922
* Please note, the `transactionTag` setting will be ignored as it is not supported for partitioned DML.
1923+
* @type array $transactionOptions Transaction options.
1924+
* {@see V1\TransactionOptions}
18921925
* }
18931926
* @return int The number of rows modified.
1927+
* @throws ValidationException
18941928
*/
18951929
public function executePartitionedUpdate($statement, array $options = [])
18961930
{
18971931
unset($options['requestOptions']['transactionTag']);
1932+
1933+
if (isset($options['transactionOptions']['isolationLevel'])) {
1934+
throw new ValidationException('Partitioned DML cannot be configured with an isolation level');
1935+
}
1936+
18981937
$session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);
18991938

19001939
$beginTransactionOptions = [
19011940
'transactionOptions' => [
19021941
'partitionedDml' => [],
19031942
]
19041943
];
1944+
19051945
if (isset($options['transactionOptions']['excludeTxnFromChangeStreams'])) {
19061946
$beginTransactionOptions['transactionOptions']['excludeTxnFromChangeStreams'] =
19071947
$options['transactionOptions']['excludeTxnFromChangeStreams'];
19081948
}
1949+
19091950
$transaction = $this->operation->transaction($session, $beginTransactionOptions);
19101951

19111952
$options = $this->addLarHeader($options);

Spanner/src/Instance.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Google\Cloud\Spanner\Connection\ConnectionInterface;
3333
use Google\Cloud\Spanner\Connection\IamInstance;
3434
use Google\Cloud\Spanner\Session\SessionPoolInterface;
35+
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
3536

3637
/**
3738
* Represents a Cloud Spanner instance
@@ -126,6 +127,11 @@ class Instance
126127
*/
127128
private $directedReadOptions;
128129

130+
/**
131+
* @var int
132+
*/
133+
private $isolationLevel;
134+
129135
/**
130136
* Create an object representing a Cloud Spanner instance.
131137
*
@@ -148,6 +154,8 @@ class Instance
148154
* {@see \Google\Cloud\Spanner\V1\DirectedReadOptions}
149155
* If using the `replicaSelection::type` setting, utilize the constants available in
150156
* {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value.
157+
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
158+
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
151159
* }
152160
*/
153161
public function __construct(
@@ -168,6 +176,7 @@ public function __construct(
168176

169177
$this->setLroProperties($lroConnection, $lroCallables, $this->name);
170178
$this->directedReadOptions = $options['directedReadOptions'] ?? [];
179+
$this->isolationLevel = $options['isolationLevel'] ?? IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED;
171180
}
172181

173182
/**
@@ -515,6 +524,8 @@ public function createDatabaseFromBackup($name, $backup, array $options = [])
515524
* @type SessionPoolInterface $sessionPool A pool used to manage
516525
* sessions.
517526
* @type string $databaseRole The user created database role which creates the session.
527+
* @type int $isolationLevel The IsolationLevel set for the transaction.
528+
* Check {@see IsolationLevel} for more details.
518529
* }
519530
* @return Database
520531
*/
@@ -530,7 +541,8 @@ public function database($name, array $options = [])
530541
isset($options['sessionPool']) ? $options['sessionPool'] : null,
531542
$this->returnInt64AsObject,
532543
isset($options['database']) ? $options['database'] : [],
533-
isset($options['databaseRole']) ? $options['databaseRole'] : ''
544+
isset($options['databaseRole']) ? $options['databaseRole'] : '',
545+
$options['isolationLevel'] ?? $this->isolationLevel,
534546
);
535547
}
536548

Spanner/src/Operation.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,7 @@ public function read(
455455
* `false`.
456456
* @type array $begin The begin transaction options.
457457
* [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions)
458+
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
458459
* }
459460
* @return Transaction
460461
*/

Spanner/src/SpannerClient.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
use Google\Cloud\Spanner\Connection\LongRunningConnection;
3737
use Google\Cloud\Spanner\Session\SessionPoolInterface;
3838
use Google\Cloud\Spanner\V1\SpannerClient as GapicSpannerClient;
39+
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
3940
use Psr\Cache\CacheItemPoolInterface;
4041
use Psr\Http\StreamInterface;
4142

@@ -138,6 +139,11 @@ class SpannerClient
138139
*/
139140
private $directedReadOptions;
140141

142+
/**
143+
* @var int
144+
*/
145+
private $isolationLevel;
146+
141147
/**
142148
* Create a Spanner client. Please note that this client requires
143149
* [the gRPC extension](https://cloud.google.com/php/grpc).
@@ -239,6 +245,8 @@ class SpannerClient
239245
* **Defaults to** `true` (enabled).
240246
* @type string $universeDomain The expected universe of the credentials. Defaults to
241247
* "googleapis.com"
248+
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
249+
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
242250
* }
243251
* @throws GoogleException If the gRPC extension is not enabled.
244252
*/
@@ -256,7 +264,8 @@ public function __construct(array $config = [])
256264
'projectIdRequired' => true,
257265
'hasEmulator' => (bool) $emulatorHost,
258266
'emulatorHost' => $emulatorHost,
259-
'queryOptions' => []
267+
'queryOptions' => [],
268+
'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED,
260269
];
261270

262271
if (!empty($config['useDiscreteBackoffs'])) {
@@ -317,6 +326,7 @@ public function __construct(array $config = [])
317326
]);
318327

319328
$this->directedReadOptions = $config['directedReadOptions'] ?? [];
329+
$this->isolationLevel = $config['isolationLevel'];
320330
}
321331

322332
/**
@@ -596,7 +606,10 @@ public function instance($name, array $instance = [])
596606
$name,
597607
$this->returnInt64AsObject,
598608
$instance,
599-
['directedReadOptions' => $this->directedReadOptions]
609+
[
610+
'directedReadOptions' => $this->directedReadOptions,
611+
'isolationLevel' => $this->isolationLevel
612+
]
600613
);
601614
}
602615

Spanner/src/Transaction.php

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ class Transaction implements TransactionalReadInterface
9898
*
9999
* @type array $begin The begin Transaction options.
100100
* [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions)
101+
* @type int $isolationLevel level of Isolation for this transaction instance
102+
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
101103
* }
102104
* @param ValueMapper $mapper Consumed internally for properly map mutation data.
103105
* @throws \InvalidArgumentException if a tag is specified on a single-use transaction.
@@ -229,14 +231,18 @@ public function getCommitStats()
229231
* parameter types. Likewise, for structs, use
230232
* {@see \Google\Cloud\Spanner\StructType}.
231233
* @type array $requestOptions Request options.
232-
* For more information on available options, please see
233-
* [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions).
234-
* Please note, if using the `priority` setting you may utilize the constants available
235-
* on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value.
236-
* Please note, the `transactionTag` setting will be ignored as the transaction tag should have already
237-
* been set when creating the transaction.
234+
* For more information on available options, please see
235+
* [the upstream documentation](https://cloud.google.com/spanner/docs/reference/rest/v1/RequestOptions).
236+
* Please note, if using the `priority` setting you may utilize the constants available
237+
* on {@see \Google\Cloud\Spanner\V1\RequestOptions\Priority} to set a value.
238+
* Please note, the `transactionTag` setting will be ignored as the transaction tag should have already
239+
* been set when creating the transaction.
240+
* @type array $transaction a set of Options for a transaction selector.
241+
* For more details on these options please
242+
* {@see https://cloud.google.com/spanner/docs/reference/rest/v1/TransactionSelector}
238243
* }
239244
* @return int The number of rows modified.
245+
* @throws ValidationException
240246
*/
241247
public function executeUpdate($sql, array $options = [])
242248
{
@@ -246,6 +252,20 @@ public function executeUpdate($sql, array $options = [])
246252
. ' This option should be set at the transaction level.'
247253
);
248254
}
255+
256+
if (isset($options['transaction']['begin']['isolationLevel']) && empty($this->transactionId)) {
257+
if (isset($options['transaction']['begin']['readOnly'])) {
258+
// isolationLevel can only be used with read/write transactions
259+
throw new ValidationException(
260+
'The isolation level can only be applied to read/write transactions.' .
261+
'Single use transactions are not read/write',
262+
);
263+
}
264+
265+
// We are planning to create a new transaction, switch to pre allocated.
266+
$this->type = self::TYPE_PRE_ALLOCATED;
267+
}
268+
249269
$options = $this->buildUpdateOptions($options);
250270
return $this->operation
251271
->executeUpdate($this->session, $this, $sql, $options);
@@ -516,6 +536,11 @@ public function isRetry()
516536
private function buildUpdateOptions(array $options): array
517537
{
518538
unset($options['requestOptions']['transactionTag']);
539+
540+
if (!empty($options['transaction']['begin'])) {
541+
$options['begin'] = $this->pluck('transaction', $options)['begin'];
542+
}
543+
519544
if (isset($this->tag)) {
520545
$options['requestOptions']['transactionTag'] = $this->tag;
521546
}

Spanner/src/TransactionConfigurationTrait.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ private function configureTransactionOptions(array $options = [])
153153
$transactionOptions['excludeTxnFromChangeStreams'] = $options['excludeTxnFromChangeStreams'];
154154
}
155155

156+
if (isset($options['isolationLevel'])) {
157+
$transactionOptions['isolationLevel'] = $options['isolationLevel'];
158+
}
159+
156160
// Allow for proper configuring of the `readLockMode` if it's set as a base or nested option
157161
if (isset($options['readLockMode'])) {
158162
$transactionOptions['readWrite']['readLockMode'] = $options['readLockMode'];

0 commit comments

Comments
 (0)