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

Commit 300968a

Browse files
committed
feat: snapshot isolation
1 parent 7cba2df commit 300968a

File tree

8 files changed

+92
-1
lines changed

8 files changed

+92
-1
lines changed

google/cloud/spanner_v1/batch.py

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

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__
@@ -136,6 +137,11 @@ class Client(ClientWithProject):
136137
or you can use the environment variable `SPANNER_ENABLE_EXTENDED_TRACING=<boolean>`
137138
to control it.
138139
140+
:type default_transaction_options: :class:`~google.cloud.spanner_v1.TransactionOptions`
141+
or :class:`dict`
142+
:param default_transaction_options: (Optional) Client options used to set the default_transaction_options
143+
used for all Read Write Transactions.
144+
139145
:raises: :class:`ValueError <exceptions.ValueError>` if both ``read_only``
140146
and ``admin`` are :data:`True`
141147
"""
@@ -157,6 +163,7 @@ def __init__(
157163
route_to_leader_enabled=True,
158164
directed_read_options=None,
159165
observability_options=None,
166+
default_transaction_options=None,
160167
):
161168
self._emulator_host = _get_spanner_emulator_host()
162169

@@ -199,6 +206,7 @@ def __init__(
199206
self._route_to_leader_enabled = route_to_leader_enabled
200207
self._directed_read_options = directed_read_options
201208
self._observability_options = observability_options
209+
self._default_transaction_options = default_transaction_options
202210

203211
@property
204212
def credentials(self):
@@ -289,6 +297,17 @@ def observability_options(self):
289297
"""
290298
return self._observability_options
291299

300+
@property
301+
def default_transaction_options(self):
302+
"""Getter for default_transaction_options.
303+
304+
:rtype:
305+
:class:`~google.cloud.spanner_v1.TransactionOptions`
306+
or :class:`dict`
307+
:returns: The default_transaction_options for the client.
308+
"""
309+
return self._default_transaction_options
310+
292311
@property
293312
def directed_read_options(self):
294313
"""Getter for directed_read_options.
@@ -434,3 +453,19 @@ def directed_read_options(self, directed_read_options):
434453
or regions should be used for non-transactional reads or queries.
435454
"""
436455
self._directed_read_options = directed_read_options
456+
457+
@default_transaction_options.setter
458+
def default_transaction_options(self, default_transaction_options):
459+
"""Sets default_transaction_options for the client
460+
:type default_transaction_options: :class:`~google.cloud.spanner_v1.TransactionOptions`
461+
or :class:`dict`
462+
:param default_transaction_options: Client options used to set the default_transaction_options
463+
used for all Read Write Transactions.
464+
"""
465+
if default_transaction_options is not None and not isinstance(
466+
default_transaction_options, TransactionOptions
467+
):
468+
raise TypeError(
469+
"default_transaction_option must be a TransactionOptions instance"
470+
)
471+
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
@@ -182,6 +182,9 @@ def __init__(
182182
self._enable_drop_protection = enable_drop_protection
183183
self._reconciling = False
184184
self._directed_read_options = self._instance._client.directed_read_options
185+
self.default_transaction_options = (
186+
self._instance._client.default_transaction_options
187+
)
185188
self._proto_descriptors = proto_descriptors
186189

187190
if pool is None:
@@ -780,6 +783,7 @@ def batch(
780783
request_options=None,
781784
max_commit_delay=None,
782785
exclude_txn_from_change_streams=False,
786+
isolation_level=None,
783787
**kw,
784788
):
785789
"""Return an object which wraps a batch.
@@ -807,14 +811,28 @@ def batch(
807811
being recorded in the change streams with the DDL option `allow_txn_exclusion` being false or
808812
unset.
809813
814+
:type isolation_level:
815+
:class:`google.cloud.spanner_v1.types.TransactionOptions.IsolationLevel`
816+
:param isolation_level:
817+
(Optional) Sets isolation level for the transaction and overrides the isolation level set at the client.
818+
810819
:rtype: :class:`~google.cloud.spanner_v1.database.BatchCheckout`
811820
:returns: new wrapper
812821
"""
822+
823+
# Set isolation level
824+
if isolation_level is None:
825+
isolation_level = (
826+
self.default_transaction_options.isolation_level
827+
if isinstance(self.default_transaction_options, TransactionOptions)
828+
else TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
829+
)
813830
return BatchCheckout(
814831
self,
815832
request_options,
816833
max_commit_delay,
817834
exclude_txn_from_change_streams,
835+
isolation_level,
818836
**kw,
819837
)
820838

@@ -886,6 +904,7 @@ def run_in_transaction(self, func, *args, **kw):
886904
from being recorded in change streams with the DDL option `allow_txn_exclusion=true`.
887905
This does not exclude the transaction from being recorded in the change streams with
888906
the DDL option `allow_txn_exclusion` being false or unset.
907+
"isolation_level" sets the isolation level for transaction.
889908
890909
:rtype: Any
891910
:returns: The return value of ``func``.
@@ -1176,6 +1195,7 @@ def __init__(
11761195
request_options=None,
11771196
max_commit_delay=None,
11781197
exclude_txn_from_change_streams=False,
1198+
isolation_level=TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
11791199
**kw,
11801200
):
11811201
self._database = database
@@ -1188,6 +1208,7 @@ def __init__(
11881208
self._request_options = request_options
11891209
self._max_commit_delay = max_commit_delay
11901210
self._exclude_txn_from_change_streams = exclude_txn_from_change_streams
1211+
self._isolation_level = isolation_level
11911212
self._kw = kw
11921213

11931214
def __enter__(self):
@@ -1209,7 +1230,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
12091230
request_options=self._request_options,
12101231
max_commit_delay=self._max_commit_delay,
12111232
exclude_txn_from_change_streams=self._exclude_txn_from_change_streams,
1212-
**self._kw,
1233+
isolation_level=self._isolation_level,
12131234
)
12141235
finally:
12151236
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

4445
DEFAULT_RETRY_TIMEOUT_SECS = 30
@@ -446,6 +447,7 @@ def run_in_transaction(self, func, *args, **kw):
446447
from being recorded in change streams with the DDL option `allow_txn_exclusion=true`.
447448
This does not exclude the transaction from being recorded in the change streams with
448449
the DDL option `allow_txn_exclusion` being false or unset.
450+
"isolation_level" sets the isolation level for transaction.
449451
450452
:rtype: Any
451453
:returns: The return value of ``func``.
@@ -460,6 +462,16 @@ def run_in_transaction(self, func, *args, **kw):
460462
exclude_txn_from_change_streams = kw.pop(
461463
"exclude_txn_from_change_streams", None
462464
)
465+
isolation_level = kw.pop("isolation_level", None)
466+
467+
if isolation_level is None:
468+
isolation_level = (
469+
self._database.default_transaction_options.isolation_level
470+
if isinstance(
471+
self._database.default_transaction_options, TransactionOptions
472+
)
473+
else TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
474+
)
463475
attempts = 0
464476

465477
observability_options = getattr(self._database, "observability_options", None)
@@ -475,6 +487,7 @@ def run_in_transaction(self, func, *args, **kw):
475487
txn.exclude_txn_from_change_streams = (
476488
exclude_txn_from_change_streams
477489
)
490+
txn.isolation_level = isolation_level
478491
else:
479492
txn = self._transaction
480493

google/cloud/spanner_v1/transaction.py

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

6263
def __init__(self, session):
6364
if session._transaction is not None:
@@ -92,6 +93,7 @@ def _make_txn_selector(self):
9293
begin=TransactionOptions(
9394
read_write=TransactionOptions.ReadWrite(),
9495
exclude_txn_from_change_streams=self.exclude_txn_from_change_streams,
96+
isolation_level=self.isolation_level,
9597
)
9698
)
9799
else:
@@ -154,6 +156,7 @@ def begin(self):
154156
txn_options = TransactionOptions(
155157
read_write=TransactionOptions.ReadWrite(),
156158
exclude_txn_from_change_streams=self.exclude_txn_from_change_streams,
159+
isolation_level=self.isolation_level,
157160
)
158161
observability_options = getattr(database, "observability_options", None)
159162
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)