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

Commit 1108b4b

Browse files
authored
Merge branch 'main' into cleanup-regex-parser
2 parents 66f6ee4 + 03400c4 commit 1108b4b

File tree

2 files changed

+157
-2
lines changed

2 files changed

+157
-2
lines changed

google/cloud/spanner_dbapi/connection.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from google.cloud.spanner_dbapi.parsed_statement import ParsedStatement, Statement
2626
from google.cloud.spanner_dbapi.transaction_helper import TransactionRetryHelper
2727
from google.cloud.spanner_dbapi.cursor import Cursor
28-
from google.cloud.spanner_v1 import RequestOptions
28+
from google.cloud.spanner_v1 import RequestOptions, TransactionOptions
2929
from google.cloud.spanner_v1.snapshot import Snapshot
3030

3131
from google.cloud.spanner_dbapi.exceptions import (
@@ -108,6 +108,7 @@ def __init__(self, instance, database=None, read_only=False, **kwargs):
108108
self._staleness = None
109109
self.request_priority = None
110110
self._transaction_begin_marked = False
111+
self._transaction_isolation_level = None
111112
# whether transaction started at Spanner. This means that we had
112113
# made at least one call to Spanner.
113114
self._spanner_transaction_started = False
@@ -279,6 +280,33 @@ def transaction_tag(self, value):
279280
"""
280281
self._connection_variables["transaction_tag"] = value
281282

283+
@property
284+
def isolation_level(self):
285+
"""The default isolation level that is used for all read/write
286+
transactions on this `Connection`.
287+
288+
Returns:
289+
google.cloud.spanner_v1.types.TransactionOptions.IsolationLevel:
290+
The isolation level that is used for read/write transactions on
291+
this `Connection`.
292+
"""
293+
return self._connection_variables.get(
294+
"isolation_level",
295+
TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
296+
)
297+
298+
@isolation_level.setter
299+
def isolation_level(self, value: TransactionOptions.IsolationLevel):
300+
"""Sets the isolation level that is used for all read/write
301+
transactions on this `Connection`.
302+
303+
Args:
304+
value (google.cloud.spanner_v1.types.TransactionOptions.IsolationLevel):
305+
The isolation level for all read/write transactions on this
306+
`Connection`.
307+
"""
308+
self._connection_variables["isolation_level"] = value
309+
282310
@property
283311
def staleness(self):
284312
"""Current read staleness option value of this `Connection`.
@@ -359,6 +387,12 @@ def transaction_checkout(self):
359387
if not self._spanner_transaction_started:
360388
self._transaction = self._session_checkout().transaction()
361389
self._transaction.transaction_tag = self.transaction_tag
390+
if self._transaction_isolation_level:
391+
self._transaction.isolation_level = (
392+
self._transaction_isolation_level
393+
)
394+
else:
395+
self._transaction.isolation_level = self.isolation_level
362396
self.transaction_tag = None
363397
self._snapshot = None
364398
self._spanner_transaction_started = True
@@ -401,7 +435,7 @@ def close(self):
401435
self.is_closed = True
402436

403437
@check_not_closed
404-
def begin(self):
438+
def begin(self, isolation_level=None):
405439
"""
406440
Marks the transaction as started.
407441
@@ -417,6 +451,7 @@ def begin(self):
417451
"is already running"
418452
)
419453
self._transaction_begin_marked = True
454+
self._transaction_isolation_level = isolation_level
420455

421456
def commit(self):
422457
"""Commits any pending transaction to the database.
@@ -461,6 +496,7 @@ def _reset_post_commit_or_rollback(self):
461496
self._release_session()
462497
self._transaction_helper.reset()
463498
self._transaction_begin_marked = False
499+
self._transaction_isolation_level = None
464500
self._spanner_transaction_started = False
465501

466502
@check_not_closed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Copyright 2025 Google LLC All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.cloud.spanner_dbapi import Connection
16+
from google.cloud.spanner_v1 import (
17+
BeginTransactionRequest,
18+
TransactionOptions,
19+
)
20+
from tests.mockserver_tests.mock_server_test_base import (
21+
MockServerTestBase,
22+
add_update_count,
23+
)
24+
25+
26+
class TestDbapiIsolationLevel(MockServerTestBase):
27+
@classmethod
28+
def setup_class(cls):
29+
super().setup_class()
30+
add_update_count("insert into singers (id, name) values (1, 'Some Singer')", 1)
31+
32+
def test_isolation_level_default(self):
33+
connection = Connection(self.instance, self.database)
34+
with connection.cursor() as cursor:
35+
cursor.execute("insert into singers (id, name) values (1, 'Some Singer')")
36+
self.assertEqual(1, cursor.rowcount)
37+
connection.commit()
38+
begin_requests = list(
39+
filter(
40+
lambda msg: isinstance(msg, BeginTransactionRequest),
41+
self.spanner_service.requests,
42+
)
43+
)
44+
self.assertEqual(1, len(begin_requests))
45+
self.assertEqual(
46+
begin_requests[0].options.isolation_level,
47+
TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
48+
)
49+
50+
def test_custom_isolation_level(self):
51+
connection = Connection(self.instance, self.database)
52+
for level in [
53+
TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
54+
TransactionOptions.IsolationLevel.REPEATABLE_READ,
55+
TransactionOptions.IsolationLevel.SERIALIZABLE,
56+
]:
57+
connection.isolation_level = level
58+
with connection.cursor() as cursor:
59+
cursor.execute(
60+
"insert into singers (id, name) values (1, 'Some Singer')"
61+
)
62+
self.assertEqual(1, cursor.rowcount)
63+
connection.commit()
64+
begin_requests = list(
65+
filter(
66+
lambda msg: isinstance(msg, BeginTransactionRequest),
67+
self.spanner_service.requests,
68+
)
69+
)
70+
self.assertEqual(1, len(begin_requests))
71+
self.assertEqual(begin_requests[0].options.isolation_level, level)
72+
MockServerTestBase.spanner_service.clear_requests()
73+
74+
def test_isolation_level_in_connection_kwargs(self):
75+
for level in [
76+
TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
77+
TransactionOptions.IsolationLevel.REPEATABLE_READ,
78+
TransactionOptions.IsolationLevel.SERIALIZABLE,
79+
]:
80+
connection = Connection(self.instance, self.database, isolation_level=level)
81+
with connection.cursor() as cursor:
82+
cursor.execute(
83+
"insert into singers (id, name) values (1, 'Some Singer')"
84+
)
85+
self.assertEqual(1, cursor.rowcount)
86+
connection.commit()
87+
begin_requests = list(
88+
filter(
89+
lambda msg: isinstance(msg, BeginTransactionRequest),
90+
self.spanner_service.requests,
91+
)
92+
)
93+
self.assertEqual(1, len(begin_requests))
94+
self.assertEqual(begin_requests[0].options.isolation_level, level)
95+
MockServerTestBase.spanner_service.clear_requests()
96+
97+
def test_transaction_isolation_level(self):
98+
connection = Connection(self.instance, self.database)
99+
for level in [
100+
TransactionOptions.IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED,
101+
TransactionOptions.IsolationLevel.REPEATABLE_READ,
102+
TransactionOptions.IsolationLevel.SERIALIZABLE,
103+
]:
104+
connection.begin(isolation_level=level)
105+
with connection.cursor() as cursor:
106+
cursor.execute(
107+
"insert into singers (id, name) values (1, 'Some Singer')"
108+
)
109+
self.assertEqual(1, cursor.rowcount)
110+
connection.commit()
111+
begin_requests = list(
112+
filter(
113+
lambda msg: isinstance(msg, BeginTransactionRequest),
114+
self.spanner_service.requests,
115+
)
116+
)
117+
self.assertEqual(1, len(begin_requests))
118+
self.assertEqual(begin_requests[0].options.isolation_level, level)
119+
MockServerTestBase.spanner_service.clear_requests()

0 commit comments

Comments
 (0)