Skip to content

Commit e535c00

Browse files
committed
proto merge for transaction options
1 parent 7cad765 commit e535c00

File tree

10 files changed

+161
-17
lines changed

10 files changed

+161
-17
lines changed

google/cloud/spanner_v1/_helpers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from google.cloud.spanner_v1 import TypeCode
3333
from google.cloud.spanner_v1 import ExecuteSqlRequest
3434
from google.cloud.spanner_v1 import JsonObject
35+
from google.cloud.spanner_v1 import TransactionOptions
3536
from google.cloud.spanner_v1.request_id_header import with_request_id
3637
from google.rpc.error_details_pb2 import RetryInfo
3738

@@ -690,3 +691,37 @@ def __radd__(self, n):
690691

691692
def _metadata_with_request_id(*args, **kwargs):
692693
return with_request_id(*args, **kwargs)
694+
695+
696+
def _merge_Transaction_Options(
697+
defaultTransactionOptions: TransactionOptions,
698+
mergeTransactionOptions: TransactionOptions,
699+
) -> TransactionOptions:
700+
"""Merges two TransactionOptions objects.
701+
702+
- Values from `mergeTransactionOptions` take precedence if set.
703+
- Values from `defaultTransactionOptions` are used only if missing.
704+
705+
Args:
706+
defaultTransactionOptions (TransactionOptions): The default transaction options (fallback values).
707+
mergeTransactionOptions (TransactionOptions): The main transaction options (overrides when set).
708+
709+
Returns:
710+
TransactionOptions: A merged TransactionOptions object.
711+
"""
712+
713+
if defaultTransactionOptions is None:
714+
return mergeTransactionOptions
715+
716+
if mergeTransactionOptions is None:
717+
return defaultTransactionOptions
718+
719+
merged_pb = TransactionOptions()._pb # Create a new protobuf object
720+
721+
# Merge defaultTransactionOptions first
722+
merged_pb.MergeFrom(defaultTransactionOptions._pb)
723+
724+
# Merge transactionOptions, ensuring it overrides default values
725+
merged_pb.MergeFrom(mergeTransactionOptions._pb)
726+
727+
return merged_pb

google/cloud/spanner_v1/batch.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from google.cloud.spanner_v1._helpers import (
2626
_metadata_with_prefix,
2727
_metadata_with_leader_aware_routing,
28+
_merge_Transaction_Options,
2829
)
2930
from google.cloud.spanner_v1._opentelemetry_tracing import trace_call
3031
from google.cloud.spanner_v1 import RequestOptions
@@ -216,6 +217,11 @@ def commit(
216217
exclude_txn_from_change_streams=exclude_txn_from_change_streams,
217218
isolation_level=isolation_level,
218219
)
220+
221+
txn_options = _merge_Transaction_Options(
222+
database.default_transaction_options.default_read_write_transaction_options,
223+
txn_options,
224+
)
219225
trace_attributes = {"num_mutations": len(self._mutations)}
220226

221227
if request_options is None:

google/cloud/spanner_v1/database.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,7 @@ def batch(
786786
request_options=None,
787787
max_commit_delay=None,
788788
exclude_txn_from_change_streams=False,
789-
isolation_level=None,
789+
isolation_level=TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
790790
**kw,
791791
):
792792
"""Return an object which wraps a batch.
@@ -823,9 +823,6 @@ def batch(
823823
:returns: new wrapper
824824
"""
825825

826-
# Set isolation level
827-
if isolation_level is None:
828-
isolation_level = self.default_transaction_options.isolation_level
829826
return BatchCheckout(
830827
self,
831828
request_options,

google/cloud/spanner_v1/session.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -469,8 +469,6 @@ def run_in_transaction(self, func, *args, **kw):
469469
)
470470
isolation_level = kw.pop("isolation_level", None)
471471

472-
if isolation_level is None:
473-
isolation_level = self._database.default_transaction_options.isolation_level
474472
attempts = 0
475473

476474
observability_options = getattr(self._database, "observability_options", None)

google/cloud/spanner_v1/transaction.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import functools
1717
import threading
1818
from google.protobuf.struct_pb2 import Struct
19+
from typing import Optional
1920

2021
from google.cloud.spanner_v1._helpers import (
2122
_make_value_pb,
@@ -24,6 +25,7 @@
2425
_metadata_with_leader_aware_routing,
2526
_retry,
2627
_check_rst_stream_error,
28+
_merge_Transaction_Options,
2729
)
2830
from google.cloud.spanner_v1 import CommitRequest
2931
from google.cloud.spanner_v1 import ExecuteBatchDmlRequest
@@ -37,7 +39,7 @@
3739
from google.cloud.spanner_v1.metrics.metrics_capture import MetricsCapture
3840
from google.api_core import gapic_v1
3941
from google.api_core.exceptions import InternalServerError
40-
from dataclasses import dataclass
42+
from dataclasses import dataclass, field
4143
from typing import Any
4244

4345

@@ -90,13 +92,17 @@ def _make_txn_selector(self):
9092
self._check_state()
9193

9294
if self._transaction_id is None:
93-
return TransactionSelector(
94-
begin=TransactionOptions(
95-
read_write=TransactionOptions.ReadWrite(),
96-
exclude_txn_from_change_streams=self.exclude_txn_from_change_streams,
97-
isolation_level=self.isolation_level,
98-
)
95+
txn_options = TransactionOptions(
96+
read_write=TransactionOptions.ReadWrite(),
97+
exclude_txn_from_change_streams=self.exclude_txn_from_change_streams,
98+
isolation_level=self.isolation_level,
99+
)
100+
101+
txn_options = _merge_Transaction_Options(
102+
self._session._database.default_transaction_options.default_read_write_transaction_options,
103+
txn_options,
99104
)
105+
return TransactionSelector(begin=txn_options)
100106
else:
101107
return TransactionSelector(id=self._transaction_id)
102108

@@ -164,6 +170,10 @@ def begin(self):
164170
exclude_txn_from_change_streams=self.exclude_txn_from_change_streams,
165171
isolation_level=self.isolation_level,
166172
)
173+
txn_options = _merge_Transaction_Options(
174+
database.default_transaction_options.default_read_write_transaction_options,
175+
txn_options,
176+
)
167177
observability_options = getattr(database, "observability_options", None)
168178
with trace_call(
169179
f"CloudSpanner.{type(self).__name__}.begin",
@@ -668,6 +678,18 @@ class BatchTransactionId:
668678

669679
@dataclass
670680
class DefaultTransactionOptions:
671-
isolation_level: TransactionOptions.IsolationLevel = (
672-
TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
681+
isolation_level: str = TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
682+
_defaultReadWriteTransactionOptions: Optional[TransactionOptions] = field(
683+
init=False, repr=False
673684
)
685+
686+
def __post_init__(self):
687+
"""Initialize _defaultReadWriteTransactionOptions automatically"""
688+
self._defaultReadWriteTransactionOptions = TransactionOptions(
689+
isolation_level=self.isolation_level
690+
)
691+
692+
@property
693+
def default_read_write_transaction_options(self) -> TransactionOptions:
694+
"""Public accessor for _defaultReadWriteTransactionOptions"""
695+
return self._defaultReadWriteTransactionOptions

tests/unit/test__helpers.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
import unittest
1717
import mock
18+
from google.cloud.spanner_v1 import TransactionOptions
1819

1920

2021
class Test_merge_query_options(unittest.TestCase):
@@ -955,3 +956,83 @@ def test(self):
955956
self.assertEqual(
956957
metadata, ("x-goog-spanner-route-to-leader", str(value).lower())
957958
)
959+
960+
961+
class Test_merge_transaction_options(unittest.TestCase):
962+
def _callFUT(self, *args, **kw):
963+
from google.cloud.spanner_v1._helpers import _merge_Transaction_Options
964+
965+
return _merge_Transaction_Options(*args, **kw)
966+
967+
def test_default_none_and_merge_none(self):
968+
default = merge = None
969+
result = self._callFUT(default, merge)
970+
self.assertIsNone(result)
971+
972+
def test_default_options_and_merge_none(self):
973+
default = TransactionOptions(
974+
isolation_level=TransactionOptions.IsolationLevel.REPEATABLE_READ
975+
)
976+
merge = None
977+
result = self._callFUT(default, merge)
978+
expected = default
979+
self.assertEqual(result, expected)
980+
981+
def test_default_none_and_merge_options(self):
982+
default = None
983+
merge = TransactionOptions(
984+
isolation_level=TransactionOptions.IsolationLevel.SERIALIZABLE
985+
)
986+
expected = merge
987+
result = self._callFUT(default, merge)
988+
self.assertEqual(result, expected)
989+
990+
def test_default_and_merge_isolation_options(self):
991+
default = TransactionOptions(
992+
isolation_level=TransactionOptions.IsolationLevel.SERIALIZABLE,
993+
read_write=TransactionOptions.ReadWrite(),
994+
)
995+
merge = TransactionOptions(
996+
isolation_level=TransactionOptions.IsolationLevel.REPEATABLE_READ,
997+
exclude_txn_from_change_streams=True,
998+
)
999+
expected = TransactionOptions(
1000+
isolation_level=TransactionOptions.IsolationLevel.REPEATABLE_READ,
1001+
read_write=TransactionOptions.ReadWrite(),
1002+
exclude_txn_from_change_streams=True,
1003+
)
1004+
result = self._callFUT(default, merge)
1005+
self.assertEqual(result, expected)
1006+
1007+
def test_default_isolation_and_merge_options(self):
1008+
default = TransactionOptions(
1009+
isolation_level=TransactionOptions.IsolationLevel.SERIALIZABLE
1010+
)
1011+
merge = TransactionOptions(
1012+
read_write=TransactionOptions.ReadWrite(),
1013+
exclude_txn_from_change_streams=True,
1014+
)
1015+
expected = TransactionOptions(
1016+
isolation_level=TransactionOptions.IsolationLevel.SERIALIZABLE,
1017+
read_write=TransactionOptions.ReadWrite(),
1018+
exclude_txn_from_change_streams=True,
1019+
)
1020+
result = self._callFUT(default, merge)
1021+
self.assertEqual(result, expected)
1022+
1023+
def test_default_isolation_and_merge_options_isolation_unspecified(self):
1024+
default = TransactionOptions(
1025+
isolation_level=TransactionOptions.IsolationLevel.SERIALIZABLE
1026+
)
1027+
merge = TransactionOptions(
1028+
read_write=TransactionOptions.ReadWrite(),
1029+
exclude_txn_from_change_streams=True,
1030+
isolation_level=TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
1031+
)
1032+
expected = TransactionOptions(
1033+
isolation_level=TransactionOptions.IsolationLevel.SERIALIZABLE,
1034+
read_write=TransactionOptions.ReadWrite(),
1035+
exclude_txn_from_change_streams=True,
1036+
)
1037+
result = self._callFUT(default, merge)
1038+
self.assertEqual(result, expected)

tests/unit/test_batch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
TransactionOptions,
2929
Mutation,
3030
BatchWriteResponse,
31+
DefaultTransactionOptions,
3132
)
3233
from google.cloud._helpers import UTC, _datetime_to_pb_timestamp
3334
import datetime
@@ -643,6 +644,7 @@ def __init__(self, enable_end_to_end_tracing=False):
643644
self._route_to_leader_enabled = True
644645
if enable_end_to_end_tracing:
645646
self.observability_options = dict(enable_end_to_end_tracing=True)
647+
default_transaction_options = DefaultTransactionOptions()
646648

647649

648650
class _FauxSpannerAPI:

tests/unit/test_database.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3161,8 +3161,8 @@ def __init__(self, name, instance=None):
31613161
from logging import Logger
31623162

31633163
self.logger = mock.create_autospec(Logger, instance=True)
3164-
self._directed_read_options = DefaultTransactionOptions()
3165-
self.default_transaction_options = None
3164+
self._directed_read_options = None
3165+
self.default_transaction_options = DefaultTransactionOptions()
31663166

31673167

31683168
class _Pool(object):

tests/unit/test_spanner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,7 @@ def __init__(self):
11051105
self._instance = _Instance()
11061106
self._route_to_leader_enabled = True
11071107
self._directed_read_options = None
1108+
self.default_transaction_options = DefaultTransactionOptions()
11081109

11091110

11101111
class _Session(object):

tests/unit/test_transaction.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import mock
1717

1818
from google.cloud.spanner_v1 import RequestOptions
19+
from google.cloud.spanner_v1 import DefaultTransactionOptions
1920
from google.cloud.spanner_v1 import Type
2021
from google.cloud.spanner_v1 import TypeCode
2122
from google.api_core.retry import Retry
@@ -1021,6 +1022,7 @@ def __init__(self):
10211022
self._instance = _Instance()
10221023
self._route_to_leader_enabled = True
10231024
self._directed_read_options = None
1025+
self.default_transaction_options = DefaultTransactionOptions()
10241026

10251027

10261028
class _Session(object):

0 commit comments

Comments
 (0)