Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dfcd5e9
Pass the isolationLevel from the SpannerClient down to operation
Hectorhammett Apr 15, 2025
3d096e4
Add support for isolationLevel to the Database class
Hectorhammett Apr 15, 2025
a5bb0d4
Update the Instance class
Hectorhammett Apr 15, 2025
e37fbca
Add configuration for Database
Hectorhammett Apr 16, 2025
98c856f
Add tests for isolationLevel
Hectorhammett Apr 17, 2025
d143215
Fix style issues
Hectorhammett Apr 17, 2025
a3f2c86
Update the executePartitionUpdate method
Hectorhammett Apr 17, 2025
1270499
Merge branch 'main' into snapshot-isolation
bshaffer May 29, 2025
095b631
Perform a transaction with the Client's isolation level when set
Hectorhammett Jun 2, 2025
f5c4c3e
Merge branch 'snapshot-isolation' of https://github.com/googleapis/go…
Hectorhammett Jun 2, 2025
5823a91
Pass the isolationLevel from the SpannerClient down to operation
Hectorhammett Apr 15, 2025
76f0512
Add support for isolationLevel to the Database class
Hectorhammett Apr 15, 2025
1185ae4
Update the Instance class
Hectorhammett Apr 15, 2025
e8d54c7
Add configuration for Database
Hectorhammett Apr 16, 2025
0689601
Add tests for isolationLevel
Hectorhammett Apr 17, 2025
80625cd
Fix style issues
Hectorhammett Apr 17, 2025
fac5460
Update the executePartitionUpdate method
Hectorhammett Apr 17, 2025
9750d98
Perform a transaction with the Client's isolation level when set
Hectorhammett Jun 2, 2025
2a230cd
Fix test issues
Hectorhammett Jun 4, 2025
f702f82
Merge branch 'snapshot-isolation' of https://github.com/googleapis/go…
Hectorhammett Jun 4, 2025
ffaecf5
Change logic for assinging IsolationLevel on runTransaction
Hectorhammett Jun 4, 2025
c24dbd5
Fix the SnippetDatabase test
Hectorhammett Jun 4, 2025
1990b03
Fix styling issues
Hectorhammett Jun 4, 2025
4c118ba
Merge branch 'main' into snapshot-isolation
bshaffer Jul 31, 2025
678c7f8
Merge branch 'main' into snapshot-isolation
bshaffer Oct 2, 2025
00e98f3
Add the correct documentation for snapshotIsolation
Hectorhammett Oct 3, 2025
6be2be9
Merge branch 'main' into snapshot-isolation
Hectorhammett Oct 4, 2025
53892bc
Remove unnecessary import
Hectorhammett Oct 4, 2025
975ef47
Update logic for transaction to handle snapshotIsolation
Hectorhammett Oct 7, 2025
a21c6a7
Removed unnecessary line
Hectorhammett Oct 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Spanner/src/Connection/Grpc.php
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,10 @@ public function beginTransaction(array $args)
$args = $this->addLarHeader($args, $this->larEnabled);
}

if (isset($transactionOptions['isolationLevel'])) {
$options->setIsolationLevel($transactionOptions['isolationLevel']);
}

// NOTE: if set for read-only actions, will throw exception
if (isset($transactionOptions['excludeTxnFromChangeStreams'])) {
$options->setExcludeTxnFromChangeStreams(
Expand Down
51 changes: 46 additions & 5 deletions Spanner/src/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
use Google\Cloud\Spanner\Session\Session;
use Google\Cloud\Spanner\Session\SessionPoolInterface;
use Google\Cloud\Spanner\V1\SpannerClient as GapicSpannerClient;
use Google\Cloud\Spanner\V1\TransactionOptions;
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
use Google\Cloud\Spanner\V1\TypeCode;
use Google\Rpc\Code;

Expand Down Expand Up @@ -192,6 +194,11 @@ class Database
*/
private $returnInt64AsObject;

/**
* @var int
*/
private int $isolationLevel;

/**
* Create an object representing a Database.
*
Expand All @@ -210,6 +217,8 @@ class Database
* be returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit
* platform compatibility. **Defaults to** false.
* @param string $databaseRole The user created database role which creates the session.
* @param int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
*/
public function __construct(
ConnectionInterface $connection,
Expand All @@ -221,7 +230,8 @@ public function __construct(
?SessionPoolInterface $sessionPool = null,
$returnInt64AsObject = false,
array $info = [],
$databaseRole = ''
$databaseRole = '',
$isolationLevel = IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
) {
$this->connection = $connection;
$this->instance = $instance;
Expand All @@ -239,6 +249,7 @@ public function __construct(
$this->databaseRole = $databaseRole;
$this->directedReadOptions = $instance->directedReadOptions();
$this->returnInt64AsObject = $returnInt64AsObject;
$this->isolationLevel = $isolationLevel;
}

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

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

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

$options['transactionOptions'] = $this->configureTransactionOptions($options['transactionOptions'] ?? []);
$transactionOptions = (isset($options['transactionOptions'])) ? $options['transactionOptions'] : [];

$options['transactionOptions'] = $this->configureTransactionOptions([
'isolationLevel' => $options['transactionOptions']['isolationLevel'] ?? $this->isolationLevel
] + $transactionOptions);

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

// Initial attempt requires to set `begin` options (ILB).
if ($attempt === 0) {
if (!isset($options['transactionOptions']['isolationLevel'])) {
$options['transactionOptions']['isolationLevel'] = IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED;
}

// Partitioned DML does not support ILB.
if (!isset($options['transactionOptions']['partitionedDml'])) {
$options['begin'] = $options['transactionOptions'];
Expand Down Expand Up @@ -1626,11 +1652,12 @@ public function delete($table, KeySet $keySet, array $options = [])
* timestamp.
* @type Duration $exactStaleness Represents a number of seconds. Executes
* all reads at a timestamp that is $exactStaleness old.
* @type bool $begin If true, will begin a new transaction. If a
* @type bool|array $begin If true, will begin a new transaction. If a
* read/write transaction is desired, set the value of
* $transactionType. If a transaction or snapshot is created, it
* will be returned as `$result->transaction()` or
* `$result->snapshot()`. **Defaults to** `false`.
* If $begin is an array {@see TransactionOptions}
* @type string $transactionType One of `SessionPoolInterface::CONTEXT_READ`
* or `SessionPoolInterface::CONTEXT_READWRITE`. If read/write is
* chosen, any snapshot options will be disregarded. If `$begin`
Expand Down Expand Up @@ -1681,6 +1708,10 @@ public function execute($sql, array $options = [])
) = $this->transactionSelector($options);
$options = $this->addLarHeader($options, true, $options['transactionContext']);

if (isset($options['transaction']['readWrite'])) {
$options['transaction']['begin']['isolationLevel'] ??= $this->isolationLevel;
}

$options['directedReadOptions'] = $this->configureDirectedReadOptions(
$options,
$this->directedReadOptions ?? []
Expand Down Expand Up @@ -1745,7 +1776,7 @@ public function mutationGroup()
* transactions.
* }
*
* @retur \Generator {@see \Google\Cloud\Spanner\V1\BatchWriteResponse}
* @return \Generator {@see \Google\Cloud\Spanner\V1\BatchWriteResponse}
*
* @throws ApiException if the remote call fails
*/
Expand Down Expand Up @@ -1889,23 +1920,33 @@ public function batchWrite(array $mutationGroups, array $options = [])
* 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.
* @type array $transactionOptions Transaction options.
* {@see V1\TransactionOptions}
* }
* @return int The number of rows modified.
* @throws ValidationException
*/
public function executePartitionedUpdate($statement, array $options = [])
{
unset($options['requestOptions']['transactionTag']);

if (isset($options['transactionOptions']['isolationLevel'])) {
throw new ValidationException('Partitioned DML cannot be configured with an isolation level');
}

$session = $this->selectSession(SessionPoolInterface::CONTEXT_READWRITE);

$beginTransactionOptions = [
'transactionOptions' => [
'partitionedDml' => [],
]
];

if (isset($options['transactionOptions']['excludeTxnFromChangeStreams'])) {
$beginTransactionOptions['transactionOptions']['excludeTxnFromChangeStreams'] =
$options['transactionOptions']['excludeTxnFromChangeStreams'];
}

$transaction = $this->operation->transaction($session, $beginTransactionOptions);

$options = $this->addLarHeader($options);
Expand Down
14 changes: 13 additions & 1 deletion Spanner/src/Instance.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Google\Cloud\Spanner\Connection\ConnectionInterface;
use Google\Cloud\Spanner\Connection\IamInstance;
use Google\Cloud\Spanner\Session\SessionPoolInterface;
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;

/**
* Represents a Cloud Spanner instance
Expand Down Expand Up @@ -126,6 +127,11 @@ class Instance
*/
private $directedReadOptions;

/**
* @var int
*/
private $isolationLevel;

/**
* Create an object representing a Cloud Spanner instance.
*
Expand All @@ -148,6 +154,8 @@ class Instance
* {@see \Google\Cloud\Spanner\V1\DirectedReadOptions}
* If using the `replicaSelection::type` setting, utilize the constants available in
* {@see \Google\Cloud\Spanner\V1\DirectedReadOptions\ReplicaSelection\Type} to set a value.
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
* }
*/
public function __construct(
Expand All @@ -168,6 +176,7 @@ public function __construct(

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

/**
Expand Down Expand Up @@ -515,6 +524,8 @@ public function createDatabaseFromBackup($name, $backup, array $options = [])
* @type SessionPoolInterface $sessionPool A pool used to manage
* sessions.
* @type string $databaseRole The user created database role which creates the session.
* @type int $isolationLevel The IsolationLevel set for the transaction.
* Check {@see IsolationLevel} for more details.
* }
* @return Database
*/
Expand All @@ -530,7 +541,8 @@ public function database($name, array $options = [])
isset($options['sessionPool']) ? $options['sessionPool'] : null,
$this->returnInt64AsObject,
isset($options['database']) ? $options['database'] : [],
isset($options['databaseRole']) ? $options['databaseRole'] : ''
isset($options['databaseRole']) ? $options['databaseRole'] : '',
$options['isolationLevel'] ?? $this->isolationLevel,
Comment thread
Hectorhammett marked this conversation as resolved.
Comment thread
bshaffer marked this conversation as resolved.
);
}

Expand Down
1 change: 1 addition & 0 deletions Spanner/src/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ public function read(
* `false`.
* @type array $begin The begin transaction options.
* [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions)
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
* }
* @return Transaction
*/
Expand Down
17 changes: 15 additions & 2 deletions Spanner/src/SpannerClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use Google\Cloud\Spanner\Connection\LongRunningConnection;
use Google\Cloud\Spanner\Session\SessionPoolInterface;
use Google\Cloud\Spanner\V1\SpannerClient as GapicSpannerClient;
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Http\StreamInterface;

Expand Down Expand Up @@ -138,6 +139,11 @@ class SpannerClient
*/
private $directedReadOptions;

/**
* @var int
*/
private $isolationLevel;

/**
* Create a Spanner client. Please note that this client requires
* [the gRPC extension](https://cloud.google.com/php/grpc).
Expand Down Expand Up @@ -239,6 +245,8 @@ class SpannerClient
* **Defaults to** `true` (enabled).
* @type string $universeDomain The expected universe of the credentials. Defaults to
* "googleapis.com"
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
* }
* @throws GoogleException If the gRPC extension is not enabled.
*/
Expand All @@ -256,7 +264,8 @@ public function __construct(array $config = [])
'projectIdRequired' => true,
'hasEmulator' => (bool) $emulatorHost,
'emulatorHost' => $emulatorHost,
'queryOptions' => []
'queryOptions' => [],
'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED,
];

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

$this->directedReadOptions = $config['directedReadOptions'] ?? [];
$this->isolationLevel = $config['isolationLevel'];
}

/**
Expand Down Expand Up @@ -596,7 +606,10 @@ public function instance($name, array $instance = [])
$name,
$this->returnInt64AsObject,
$instance,
['directedReadOptions' => $this->directedReadOptions]
[
'directedReadOptions' => $this->directedReadOptions,
'isolationLevel' => $this->isolationLevel
]
);
}

Expand Down
37 changes: 31 additions & 6 deletions Spanner/src/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ class Transaction implements TransactionalReadInterface
*
* @type array $begin The begin Transaction options.
* [Refer](https://cloud.google.com/spanner/docs/reference/rpc/google.spanner.v1#transactionoptions)
* @type int $isolationLevel level of Isolation for this transaction instance
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
* }
* @param ValueMapper $mapper Consumed internally for properly map mutation data.
* @throws \InvalidArgumentException if a tag is specified on a single-use transaction.
Expand Down Expand Up @@ -229,14 +231,18 @@ public function getCommitStats()
* 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 the transaction tag should have already
* been set when creating the transaction.
* 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.
* @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}
* }
* @return int The number of rows modified.
* @throws ValidationException
*/
public function executeUpdate($sql, array $options = [])
{
Expand All @@ -246,6 +252,20 @@ public function executeUpdate($sql, array $options = [])
. ' This option should be set at the transaction level.'
);
}

if (isset($options['transaction']['begin']['isolationLevel']) && empty($this->transactionId)) {
if (isset($options['transaction']['begin']['readOnly'])) {
// isolationLevel can only be used with read/write transactions
throw new ValidationException(
'The isolation level can only be applied to read/write transactions.' .
'Single use transactions are not read/write',
);
}

// We are planning to create a new transaction, switch to pre allocated.
$this->type = self::TYPE_PRE_ALLOCATED;
}

$options = $this->buildUpdateOptions($options);
return $this->operation
->executeUpdate($this->session, $this, $sql, $options);
Expand Down Expand Up @@ -516,6 +536,11 @@ public function isRetry()
private function buildUpdateOptions(array $options): array
{
unset($options['requestOptions']['transactionTag']);

if (!empty($options['transaction']['begin'])) {
$options['begin'] = $this->pluck('transaction', $options)['begin'];
}

if (isset($this->tag)) {
$options['requestOptions']['transactionTag'] = $this->tag;
}
Expand Down
4 changes: 4 additions & 0 deletions Spanner/src/TransactionConfigurationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ private function configureTransactionOptions(array $options = [])
$transactionOptions['excludeTxnFromChangeStreams'] = $options['excludeTxnFromChangeStreams'];
}

if (isset($options['isolationLevel'])) {
$transactionOptions['isolationLevel'] = $options['isolationLevel'];
}
Comment thread
Hectorhammett marked this conversation as resolved.

// Allow for proper configuring of the `readLockMode` if it's set as a base or nested option
if (isset($options['readLockMode'])) {
$transactionOptions['readWrite']['readLockMode'] = $options['readLockMode'];
Expand Down
Loading
Loading