Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 33 additions & 3 deletions Spanner/src/Database.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
use Google\Cloud\Spanner\V1\Mutation;
use Google\Cloud\Spanner\V1\Mutation\Delete;
use Google\Cloud\Spanner\V1\Mutation\Write;
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
use Google\Cloud\Spanner\V1\TypeCode;
use Google\LongRunning\ListOperationsRequest;
use Google\LongRunning\Operation as OperationProto;
Expand All @@ -59,6 +60,7 @@
use Google\Protobuf\Value;
use Google\Rpc\Code;
use Psr\Cache\CacheItemPoolInterface;
use InvalidArgumentException;

/**
* Represents a Cloud Spanner Database.
Expand Down Expand Up @@ -123,6 +125,7 @@ class Database
private bool $returnInt64AsObject;
private CacheItemPoolInterface $cacheItemPool;
private array $info;
private int $isolationLevel;

private const MUTATION_SETTERS = [
'insert' => 'setInsert',
Expand All @@ -132,6 +135,7 @@ class Database
'delete' => 'setDelete'
];


/**
* Create an object representing a Database.
*
Expand All @@ -156,6 +160,8 @@ class Database
* @type string $databaseRole The user created database role which
* creates the session.
* @type array $database The database info.
* @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 Down Expand Up @@ -187,6 +193,7 @@ public function __construct(

$this->optionsValidator = new OptionsValidator($serializer);
$this->directedReadOptions = $instance->directedReadOptions();
$this->isolationLevel = $options['isolationLevel'] ?? IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED;
}

/**
Expand Down Expand Up @@ -802,6 +809,8 @@ public function snapshot(array $options = []): TransactionalReadInterface
* 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 @@ -813,7 +822,9 @@ public function transaction(array $options = []): Transaction
throw new \BadMethodCallException('Nested transactions are not supported by this client.');
}

$options['transactionOptions'] = $this->initReadWriteTransactionOptions();
$options['transactionOptions'] = $this->configureReadWriteTransactionOptions([
'isolationLevel' => $options['isolationLevel'] ?? $this->isolationLevel
]);

return $this->operation->transaction($this->session, $options);
}
Expand Down Expand Up @@ -899,6 +910,9 @@ public function transaction(array $options = []): Transaction
* 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 @@ -913,7 +927,11 @@ public function runTransaction(callable $operation, array $options = []): mixed
$retrySettings = $options['retrySettings'] ?? ['maxRetries' => self::MAX_RETRIES];
$maxRetries = $retrySettings instanceof RetrySettings
? $retrySettings->getMaxRetries()
: $retrySettings['maxRetries'];
: $retrySettings['maxRetries'];

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

// Configure necessary readWrite nested and base options
$options['transactionOptions'] = $this->configureReadWriteTransactionOptions(
Expand All @@ -924,6 +942,10 @@ public function runTransaction(callable $operation, array $options = []): mixed
$startTransactionFn = function ($options) use (&$attempt) {
// 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 @@ -1614,7 +1636,7 @@ public function delete(string $table, KeySet $keySet, array $options = []): Time
* 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
Expand Down Expand Up @@ -1665,6 +1687,10 @@ public function execute($sql, array $options = []): Result
$options['transactionContext']
) = $this->transactionSelector($options);

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

$options['directedReadOptions'] = $this->configureDirectedReadOptions(
$options,
$this->directedReadOptions
Expand Down Expand Up @@ -1890,12 +1916,16 @@ public function batchWrite(array $mutationGroups, array $options = []): \Generat
public function executePartitionedUpdate($statement, array $options = []): int
{
unset($options['requestOptions']['transactionTag']);
if (isset($options['transactionOptions']['isolationLevel'])) {
throw new InvalidArgumentException('Partitioned DML cannot be configured with an isolation level');
}

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

if (isset($options['transactionOptions']['excludeTxnFromChangeStreams'])) {
$beginTransactionOptions['transactionOptions']['excludeTxnFromChangeStreams'] =
$options['transactionOptions']['excludeTxnFromChangeStreams'];
Expand Down
10 changes: 10 additions & 0 deletions Spanner/src/Instance.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
use Google\Cloud\Spanner\Admin\Instance\V1\UpdateInstanceRequest;
use Google\Cloud\Spanner\Session\SessionCache;
use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient;
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
use Google\LongRunning\ListOperationsRequest;
use Google\LongRunning\Operation as OperationProto;
use Psr\Cache\CacheItemPoolInterface;
Expand Down Expand Up @@ -73,6 +74,11 @@ class Instance
private bool $returnInt64AsObject;
private CacheItemPoolInterface|null $cacheItemPool;

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

/**
* Create an object representing a Cloud Spanner instance.
*
Expand All @@ -97,6 +103,8 @@ class Instance
* returned as a {@see \Google\Cloud\Core\Int64} object for 32 bit platform
* compatibility. **Defaults to** false.
* @type CacheItemPool $cacheItemPool
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
* **Defaults to** IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
* }
* @param array $info A representation of the instance object.
*/
Expand All @@ -118,6 +126,7 @@ public function __construct(
$this->cacheItemPool = $options['cacheItemPool'] ?? null;
$this->projectName = InstanceAdminClient::projectName($projectId);
$this->optionsValidator = new OptionsValidator($serializer);
$this->isolationLevel = $options['isolationLevel'] ?? IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED;
}

/**
Expand Down Expand Up @@ -552,6 +561,7 @@ public function database(string $name, array $options = []): Database
'routeToLeader' => $this->routeToLeader,
'defaultQueryOptions' => $this->defaultQueryOptions,
'returnInt64AsObject' => $this->returnInt64AsObject,
'isolationLevel' => $this->isolationLevel,
]
);
}
Expand Down
1 change: 1 addition & 0 deletions Spanner/src/Operation.php
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ public function read(
* that commit mutations but do not perform any reads or queries. If not supplied,
* one of the mutations from the mutation set will be selected and sent as a part of
* this request.
* @type int $isolationLevel The level of Isolation for the transactions executed by this Client's instance.
* }
* @return Transaction
*/
Expand Down
11 changes: 11 additions & 0 deletions Spanner/src/SpannerClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use Google\Cloud\Spanner\Batch\BatchClient;
use Google\Cloud\Spanner\Middleware\SpannerMiddleware;
use Google\Cloud\Spanner\V1\Client\SpannerClient as GapicSpannerClient;
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
use Google\LongRunning\Operation as OperationProto;
use Google\Protobuf\Duration;
use Psr\Cache\CacheItemPoolInterface;
Expand Down Expand Up @@ -131,6 +132,11 @@
private array $defaultQueryOptions;
private CacheItemPoolInterface|null $cacheItemPool;

/**
* @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 @@ -180,6 +186,8 @@
* @type string $universeDomain The expected universe of the credentials. Defaults to
* "googleapis.com"
* @type CacheItemPoolInterface $cacheItemPool
* @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 @@ -196,6 +204,7 @@
'emulatorHost' => $emulatorHost,
'queryOptions' => [],
'cacheItemPool' => null,
'isolationLevel' => IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED,
];

$this->returnInt64AsObject = $options['returnInt64AsObject'];
Expand Down Expand Up @@ -254,6 +263,7 @@

$this->projectName = InstanceAdminClient::projectName($this->projectId);
$this->cacheItemPool = $options['cacheItemPool'];
$this->isolationLevel = $config['isolationLevel'];

Check failure on line 266 in Spanner/src/SpannerClient.php

View workflow job for this annotation

GitHub Actions / PHPStan Static Analysis

Undefined variable: $config
}

/**
Expand Down Expand Up @@ -561,6 +571,7 @@
'defaultQueryOptions' => $this->defaultQueryOptions,
'returnInt64AsObject' => $this->returnInt64AsObject,
'cacheItemPool' => $this->cacheItemPool,
'isolationLevel' => $this->isolationLevel,
],
$instance,
);
Expand Down
13 changes: 13 additions & 0 deletions Spanner/src/Transaction.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ class Transaction implements TransactionalReadInterface
* @type array $singleUse The singleUse Transaction options. See {@see V1\TransactionOptions}.
* @type array $requestOptions See {@see V1\RequestOptions}.
* @type array $transactionOptions See {@see 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 @@ -241,6 +243,17 @@ public function executeUpdate(string $sql, array $options = []): int
. ' This option should be set at the transaction level.'
);
}

if ($this->type() === self::TYPE_SINGLE_USE &&
isset($options['transaction']['begin']['isolationLevel']) ||
isset($options['transaction']['single_use']['isolationLevel'])
) {
throw new ValidationException(
'The isolation level can only be applied to read/write transactions.' .
'Single use transactions are not read/write',
);
}

$options = $this->buildUpdateOptions($options);
return $this->operation
->executeUpdate($this->session, $this, $sql, $options);
Expand Down
6 changes: 5 additions & 1 deletion Spanner/src/TransactionConfigurationTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,13 @@ private function configureReadWriteTransactionOptions(array|TransactionOptions $
{
$excludeTxn = $options instanceof TransactionOptions
? $options->getExcludeTxnFromChangeStreams()
: $options['excludeTxnFromChangeStreams'] ?? null;
: $options['excludeTxnFromChangeStreams'] ?? null;
$isolationLevel = $options instanceof TransactionOptions
? $options->getIsolationLevel()
: $options['isolationLevel'] ?? null;
return array_filter([
'excludeTxnFromChangeStreams' => $excludeTxn,
'isolationLevel' => $isolationLevel,
]) + $this->initReadWriteTransactionOptions();
}

Expand Down
5 changes: 5 additions & 0 deletions Spanner/tests/Snippet/DatabaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
use Google\Cloud\Spanner\V1\RollbackRequest;
use Google\Cloud\Spanner\V1\Session;
use Google\Cloud\Spanner\V1\Transaction as TransactionProto;
use Google\Cloud\Spanner\V1\TransactionOptions\IsolationLevel;
use Google\LongRunning\Client\OperationsClient;
use Google\LongRunning\ListOperationsResponse;
use Google\LongRunning\Operation;
Expand Down Expand Up @@ -491,6 +492,10 @@ public function testRunTransactionRollback()
$this->serializer->encodeMessage($request)['transaction']['begin']['readWrite'],
['readLockMode' => 0, 'multiplexedSessionPreviousTransactionId' => '']
);
$this->assertEquals(
$this->serializer->encodeMessage($request)['transaction']['begin']['isolationLevel'],
IsolationLevel::ISOLATION_LEVEL_UNSPECIFIED
);
return true;
}),
Argument::type('array')
Expand Down
2 changes: 1 addition & 1 deletion Spanner/tests/System/TransactionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ public function testRunTransactionILBWithMultipleOperations()
'id' => $id,
'name' => uniqid(self::TESTING_PREFIX),
'birthday' => new Date(new \DateTime())
]
],
]
);
$transactionId = $t->id();
Expand Down
Loading
Loading