Skip to content

Commit 497bf85

Browse files
committed
proto merge for transaction options
1 parent 5d7c9c4 commit 497bf85

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

@@ -644,3 +645,37 @@ def __radd__(self, n):
644645

645646
def _metadata_with_request_id(*args, **kwargs):
646647
return with_request_id(*args, **kwargs)
648+
649+
650+
def _merge_Transaction_Options(
651+
defaultTransactionOptions: TransactionOptions,
652+
mergeTransactionOptions: TransactionOptions,
653+
) -> TransactionOptions:
654+
"""Merges two TransactionOptions objects.
655+
656+
- Values from `mergeTransactionOptions` take precedence if set.
657+
- Values from `defaultTransactionOptions` are used only if missing.
658+
659+
Args:
660+
defaultTransactionOptions (TransactionOptions): The default transaction options (fallback values).
661+
mergeTransactionOptions (TransactionOptions): The main transaction options (overrides when set).
662+
663+
Returns:
664+
TransactionOptions: A merged TransactionOptions object.
665+
"""
666+
667+
if defaultTransactionOptions is None:
668+
return mergeTransactionOptions
669+
670+
if mergeTransactionOptions is None:
671+
return defaultTransactionOptions
672+
673+
merged_pb = TransactionOptions()._pb # Create a new protobuf object
674+
675+
# Merge defaultTransactionOptions first
676+
merged_pb.MergeFrom(defaultTransactionOptions._pb)
677+
678+
# Merge transactionOptions, ensuring it overrides default values
679+
merged_pb.MergeFrom(mergeTransactionOptions._pb)
680+
681+
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
@@ -785,7 +785,7 @@ def batch(
785785
request_options=None,
786786
max_commit_delay=None,
787787
exclude_txn_from_change_streams=False,
788-
isolation_level=None,
788+
isolation_level=TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
789789
**kw,
790790
):
791791
"""Return an object which wraps a batch.
@@ -822,9 +822,6 @@ def batch(
822822
:returns: new wrapper
823823
"""
824824

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

google/cloud/spanner_v1/session.py

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

467-
if isolation_level is None:
468-
isolation_level = self._database.default_transaction_options.isolation_level
469467
attempts = 0
470468

471469
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

@@ -159,6 +165,10 @@ def begin(self):
159165
exclude_txn_from_change_streams=self.exclude_txn_from_change_streams,
160166
isolation_level=self.isolation_level,
161167
)
168+
txn_options = _merge_Transaction_Options(
169+
database.default_transaction_options.default_read_write_transaction_options,
170+
txn_options,
171+
)
162172
observability_options = getattr(database, "observability_options", None)
163173
with trace_call(
164174
f"CloudSpanner.{type(self).__name__}.begin",
@@ -659,6 +669,18 @@ class BatchTransactionId:
659669

660670
@dataclass
661671
class DefaultTransactionOptions:
662-
isolation_level: TransactionOptions.IsolationLevel = (
663-
TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
672+
isolation_level: str = TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED
673+
_defaultReadWriteTransactionOptions: Optional[TransactionOptions] = field(
674+
init=False, repr=False
664675
)
676+
677+
def __post_init__(self):
678+
"""Initialize _defaultReadWriteTransactionOptions automatically"""
679+
self._defaultReadWriteTransactionOptions = TransactionOptions(
680+
isolation_level=self.isolation_level
681+
)
682+
683+
@property
684+
def default_read_write_transaction_options(self) -> TransactionOptions:
685+
"""Public accessor for _defaultReadWriteTransactionOptions"""
686+
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
@@ -27,6 +27,7 @@
2727
TransactionOptions,
2828
Mutation,
2929
BatchWriteResponse,
30+
DefaultTransactionOptions,
3031
)
3132
from google.cloud._helpers import UTC, _datetime_to_pb_timestamp
3233
import datetime
@@ -624,6 +625,7 @@ def session_id(self):
624625
class _Database(object):
625626
name = "testing"
626627
_route_to_leader_enabled = True
628+
default_transaction_options = (DefaultTransactionOptions(),)
627629

628630

629631
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
@@ -1015,6 +1016,7 @@ def __init__(self):
10151016
self._instance = _Instance()
10161017
self._route_to_leader_enabled = True
10171018
self._directed_read_options = None
1019+
self.default_transaction_options = DefaultTransactionOptions()
10181020

10191021

10201022
class _Session(object):

0 commit comments

Comments
 (0)