Skip to content
This repository was archived by the owner on Mar 31, 2026. It is now read-only.

Commit 5c1bb24

Browse files
committed
feat: snapshot isolation
1 parent fb21d9a commit 5c1bb24

8 files changed

Lines changed: 92 additions & 1 deletion

File tree

google/cloud/spanner_v1/batch.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ def commit(
167167
request_options=None,
168168
max_commit_delay=None,
169169
exclude_txn_from_change_streams=False,
170+
isolation_level=TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
170171
**kwargs,
171172
):
172173
"""Commit mutations to the database.
@@ -187,6 +188,18 @@ def commit(
187188
(Optional) The amount of latency this request is willing to incur
188189
in order to improve throughput.
189190
191+
:type exclude_txn_from_change_streams: bool
192+
:param exclude_txn_from_change_streams:
193+
(Optional) If true, instructs the transaction to be excluded from being recorded in change streams
194+
with the DDL option `allow_txn_exclusion=true`. This does not exclude the transaction from
195+
being recorded in the change streams with the DDL option `allow_txn_exclusion` being false or
196+
unset.
197+
198+
:type isolation_level:
199+
:class:`google.cloud.spanner_v1.types.TransactionOptions.IsolationLevel`
200+
:param isolation_level:
201+
(Optional) Sets isolation level for the transaction.
202+
190203
:rtype: datetime
191204
:returns: timestamp of the committed changes.
192205
"""
@@ -201,6 +214,7 @@ def commit(
201214
txn_options = TransactionOptions(
202215
read_write=TransactionOptions.ReadWrite(),
203216
exclude_txn_from_change_streams=exclude_txn_from_change_streams,
217+
isolation_level=isolation_level,
204218
)
205219
trace_attributes = {"num_mutations": len(self._mutations)}
206220

google/cloud/spanner_v1/client.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from google.cloud.spanner_admin_instance_v1.services.instance_admin.transports.grpc import (
4242
InstanceAdminGrpcTransport,
4343
)
44+
from google.cloud.spanner_v1 import TransactionOptions
4445
from google.cloud.spanner_admin_instance_v1 import ListInstanceConfigsRequest
4546
from google.cloud.spanner_admin_instance_v1 import ListInstancesRequest
4647
from google.cloud.spanner_v1 import __version__
@@ -161,6 +162,11 @@ class Client(ClientWithProject):
161162
or you can use the environment variable `SPANNER_ENABLE_EXTENDED_TRACING=<boolean>`
162163
to control it.
163164
165+
:type default_transaction_options: :class:`~google.cloud.spanner_v1.TransactionOptions`
166+
or :class:`dict`
167+
:param default_transaction_options: (Optional) Client options used to set the default_transaction_options
168+
used for all Read Write Transactions.
169+
164170
:raises: :class:`ValueError <exceptions.ValueError>` if both ``read_only``
165171
and ``admin`` are :data:`True`
166172
"""
@@ -182,6 +188,7 @@ def __init__(
182188
route_to_leader_enabled=True,
183189
directed_read_options=None,
184190
observability_options=None,
191+
default_transaction_options=None,
185192
):
186193
self._emulator_host = _get_spanner_emulator_host()
187194

@@ -243,6 +250,7 @@ def __init__(
243250
self._route_to_leader_enabled = route_to_leader_enabled
244251
self._directed_read_options = directed_read_options
245252
self._observability_options = observability_options
253+
self._default_transaction_options = default_transaction_options
246254

247255
@property
248256
def credentials(self):
@@ -333,6 +341,17 @@ def observability_options(self):
333341
"""
334342
return self._observability_options
335343

344+
@property
345+
def default_transaction_options(self):
346+
"""Getter for default_transaction_options.
347+
348+
:rtype:
349+
:class:`~google.cloud.spanner_v1.TransactionOptions`
350+
or :class:`dict`
351+
:returns: The default_transaction_options for the client.
352+
"""
353+
return self._default_transaction_options
354+
336355
@property
337356
def directed_read_options(self):
338357
"""Getter for directed_read_options.
@@ -478,3 +497,19 @@ def directed_read_options(self, directed_read_options):
478497
or regions should be used for non-transactional reads or queries.
479498
"""
480499
self._directed_read_options = directed_read_options
500+
501+
@default_transaction_options.setter
502+
def default_transaction_options(self, default_transaction_options):
503+
"""Sets default_transaction_options for the client
504+
:type default_transaction_options: :class:`~google.cloud.spanner_v1.TransactionOptions`
505+
or :class:`dict`
506+
:param default_transaction_options: Client options used to set the default_transaction_options
507+
used for all Read Write Transactions.
508+
"""
509+
if default_transaction_options is not None and not isinstance(
510+
default_transaction_options, TransactionOptions
511+
):
512+
raise TypeError(
513+
"default_transaction_option must be a TransactionOptions instance"
514+
)
515+
self._default_transaction_options = default_transaction_options

google/cloud/spanner_v1/database.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,9 @@ def __init__(
183183
self._enable_drop_protection = enable_drop_protection
184184
self._reconciling = False
185185
self._directed_read_options = self._instance._client.directed_read_options
186+
self.default_transaction_options = (
187+
self._instance._client.default_transaction_options
188+
)
186189
self._proto_descriptors = proto_descriptors
187190

188191
if pool is None:
@@ -781,6 +784,7 @@ def batch(
781784
request_options=None,
782785
max_commit_delay=None,
783786
exclude_txn_from_change_streams=False,
787+
isolation_level=None,
784788
**kw,
785789
):
786790
"""Return an object which wraps a batch.
@@ -808,14 +812,28 @@ def batch(
808812
being recorded in the change streams with the DDL option `allow_txn_exclusion` being false or
809813
unset.
810814
815+
:type isolation_level:
816+
:class:`google.cloud.spanner_v1.types.TransactionOptions.IsolationLevel`
817+
:param isolation_level:
818+
(Optional) Sets isolation level for the transaction and overrides the isolation level set at the client.
819+
811820
:rtype: :class:`~google.cloud.spanner_v1.database.BatchCheckout`
812821
:returns: new wrapper
813822
"""
823+
824+
# Set isolation level
825+
if isolation_level is None:
826+
isolation_level = (
827+
self.default_transaction_options.isolation_level
828+
if isinstance(self.default_transaction_options, TransactionOptions)
829+
else TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
830+
)
814831
return BatchCheckout(
815832
self,
816833
request_options,
817834
max_commit_delay,
818835
exclude_txn_from_change_streams,
836+
isolation_level,
819837
**kw,
820838
)
821839

@@ -887,6 +905,7 @@ def run_in_transaction(self, func, *args, **kw):
887905
from being recorded in change streams with the DDL option `allow_txn_exclusion=true`.
888906
This does not exclude the transaction from being recorded in the change streams with
889907
the DDL option `allow_txn_exclusion` being false or unset.
908+
"isolation_level" sets the isolation level for transaction.
890909
891910
:rtype: Any
892911
:returns: The return value of ``func``.
@@ -1177,6 +1196,7 @@ def __init__(
11771196
request_options=None,
11781197
max_commit_delay=None,
11791198
exclude_txn_from_change_streams=False,
1199+
isolation_level=TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
11801200
**kw,
11811201
):
11821202
self._database = database
@@ -1189,6 +1209,7 @@ def __init__(
11891209
self._request_options = request_options
11901210
self._max_commit_delay = max_commit_delay
11911211
self._exclude_txn_from_change_streams = exclude_txn_from_change_streams
1212+
self._isolation_level = isolation_level
11921213
self._kw = kw
11931214

11941215
def __enter__(self):
@@ -1210,7 +1231,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
12101231
request_options=self._request_options,
12111232
max_commit_delay=self._max_commit_delay,
12121233
exclude_txn_from_change_streams=self._exclude_txn_from_change_streams,
1213-
**self._kw,
1234+
isolation_level=self._isolation_level,
12141235
)
12151236
finally:
12161237
if self._database.log_commit_stats and self._batch.commit_stats:

google/cloud/spanner_v1/session.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from google.cloud.spanner_v1.batch import Batch
4040
from google.cloud.spanner_v1.snapshot import Snapshot
4141
from google.cloud.spanner_v1.transaction import Transaction
42+
from google.cloud.spanner_v1 import TransactionOptions
4243

4344
from google.cloud.spanner_v1.metrics.metrics_capture import MetricsCapture
4445

@@ -448,6 +449,7 @@ def run_in_transaction(self, func, *args, **kw):
448449
from being recorded in change streams with the DDL option `allow_txn_exclusion=true`.
449450
This does not exclude the transaction from being recorded in the change streams with
450451
the DDL option `allow_txn_exclusion` being false or unset.
452+
"isolation_level" sets the isolation level for transaction.
451453
452454
:rtype: Any
453455
:returns: The return value of ``func``.
@@ -462,6 +464,16 @@ def run_in_transaction(self, func, *args, **kw):
462464
exclude_txn_from_change_streams = kw.pop(
463465
"exclude_txn_from_change_streams", None
464466
)
467+
isolation_level = kw.pop("isolation_level", None)
468+
469+
if isolation_level is None:
470+
isolation_level = (
471+
self._database.default_transaction_options.isolation_level
472+
if isinstance(
473+
self._database.default_transaction_options, TransactionOptions
474+
)
475+
else TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
476+
)
465477
attempts = 0
466478

467479
observability_options = getattr(self._database, "observability_options", None)
@@ -477,6 +489,7 @@ def run_in_transaction(self, func, *args, **kw):
477489
txn.exclude_txn_from_change_streams = (
478490
exclude_txn_from_change_streams
479491
)
492+
txn.isolation_level = isolation_level
480493
else:
481494
txn = self._transaction
482495

google/cloud/spanner_v1/transaction.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class Transaction(_SnapshotBase, _BatchBase):
5959
_lock = threading.Lock()
6060
_read_only = False
6161
exclude_txn_from_change_streams = False
62+
isolation_level = TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
6263

6364
def __init__(self, session):
6465
if session._transaction is not None:
@@ -93,6 +94,7 @@ def _make_txn_selector(self):
9394
begin=TransactionOptions(
9495
read_write=TransactionOptions.ReadWrite(),
9596
exclude_txn_from_change_streams=self.exclude_txn_from_change_streams,
97+
isolation_level=self.isolation_level,
9698
)
9799
)
98100
else:
@@ -155,6 +157,7 @@ def begin(self):
155157
txn_options = TransactionOptions(
156158
read_write=TransactionOptions.ReadWrite(),
157159
exclude_txn_from_change_streams=self.exclude_txn_from_change_streams,
160+
isolation_level=self.isolation_level,
158161
)
159162
observability_options = getattr(database, "observability_options", None)
160163
with trace_call(

tests/unit/test_database.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3116,6 +3116,7 @@ def __init__(
31163116
project=TestDatabase.PROJECT_ID,
31173117
route_to_leader_enabled=True,
31183118
directed_read_options=None,
3119+
default_transaction_options=None,
31193120
):
31203121
from google.cloud.spanner_v1 import ExecuteSqlRequest
31213122

@@ -3129,6 +3130,7 @@ def __init__(
31293130
self._query_options = ExecuteSqlRequest.QueryOptions(optimizer_version="1")
31303131
self.route_to_leader_enabled = route_to_leader_enabled
31313132
self.directed_read_options = directed_read_options
3133+
self.default_transaction_options = default_transaction_options
31323134

31333135

31343136
class _Instance(object):
@@ -3156,6 +3158,7 @@ def __init__(self, name, instance=None):
31563158

31573159
self.logger = mock.create_autospec(Logger, instance=True)
31583160
self._directed_read_options = None
3161+
self.default_transaction_options = None
31593162

31603163

31613164
class _Pool(object):

tests/unit/test_instance.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,7 @@ def __init__(self, project, timeout_seconds=None):
10191019
self.timeout_seconds = timeout_seconds
10201020
self.route_to_leader_enabled = True
10211021
self.directed_read_options = None
1022+
self.default_transaction_options = None
10221023

10231024
def copy(self):
10241025
from copy import deepcopy

tests/unit/test_session.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def _make_database(name=DATABASE_NAME, database_role=None):
7070
database.log_commit_stats = False
7171
database.database_role = database_role
7272
database._route_to_leader_enabled = True
73+
database.default_transaction_options = None
7374
return database
7475

7576
@staticmethod

0 commit comments

Comments
 (0)