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

Commit e2cbd36

Browse files
authored
Merge branch 'main' into splits
2 parents b12951d + beb33d2 commit e2cbd36

File tree

7 files changed

+191
-222
lines changed

7 files changed

+191
-222
lines changed

.github/.OwlBot.lock.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@
1313
# limitations under the License.
1414
docker:
1515
image: gcr.io/cloud-devrel-public-resources/owlbot-python:latest
16-
digest: sha256:5581906b957284864632cde4e9c51d1cc66b0094990b27e689132fe5cd036046
17-
# created: 2025-03-05
16+
digest: sha256:25de45b58e52021d3a24a6273964371a97a4efeefe6ad3845a64e697c63b6447
17+
# created: 2025-04-14T14:34:43.260858345Z

google/cloud/spanner_dbapi/client_side_statement_executor.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
from typing import TYPE_CHECKING
14+
from typing import TYPE_CHECKING, Union
15+
from google.cloud.spanner_v1 import TransactionOptions
1516

1617
if TYPE_CHECKING:
1718
from google.cloud.spanner_dbapi.cursor import Cursor
@@ -58,7 +59,7 @@ def execute(cursor: "Cursor", parsed_statement: ParsedStatement):
5859
connection.commit()
5960
return None
6061
if statement_type == ClientSideStatementType.BEGIN:
61-
connection.begin()
62+
connection.begin(isolation_level=_get_isolation_level(parsed_statement))
6263
return None
6364
if statement_type == ClientSideStatementType.ROLLBACK:
6465
connection.rollback()
@@ -121,3 +122,19 @@ def _get_streamed_result_set(column_name, type_code, column_values):
121122
column_values_pb.append(_make_value_pb(column_value))
122123
result_set.values.extend(column_values_pb)
123124
return StreamedResultSet(iter([result_set]))
125+
126+
127+
def _get_isolation_level(
128+
statement: ParsedStatement,
129+
) -> Union[TransactionOptions.IsolationLevel, None]:
130+
if (
131+
statement.client_side_statement_params is None
132+
or len(statement.client_side_statement_params) == 0
133+
):
134+
return None
135+
level = statement.client_side_statement_params[0]
136+
if not isinstance(level, str) or level == "":
137+
return None
138+
# Replace (duplicate) whitespaces in the string with an underscore.
139+
level = "_".join(level.split()).upper()
140+
return TransactionOptions.IsolationLevel[level]

google/cloud/spanner_dbapi/client_side_statement_parser.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
Statement,
2222
)
2323

24-
RE_BEGIN = re.compile(r"^\s*(BEGIN|START)(\s+TRANSACTION)?\s*$", re.IGNORECASE)
24+
RE_BEGIN = re.compile(
25+
r"^\s*(?:BEGIN|START)(?:\s+TRANSACTION)?(?:\s+ISOLATION\s+LEVEL\s+(REPEATABLE\s+READ|SERIALIZABLE))?\s*$",
26+
re.IGNORECASE,
27+
)
2528
RE_COMMIT = re.compile(r"^\s*(COMMIT)(\s+TRANSACTION)?\s*$", re.IGNORECASE)
2629
RE_ROLLBACK = re.compile(r"^\s*(ROLLBACK)(\s+TRANSACTION)?\s*$", re.IGNORECASE)
2730
RE_SHOW_COMMIT_TIMESTAMP = re.compile(
@@ -68,6 +71,10 @@ def parse_stmt(query):
6871
elif RE_START_BATCH_DML.match(query):
6972
client_side_statement_type = ClientSideStatementType.START_BATCH_DML
7073
elif RE_BEGIN.match(query):
74+
match = re.search(RE_BEGIN, query)
75+
isolation_level = match.group(1)
76+
if isolation_level is not None:
77+
client_side_statement_params.append(isolation_level)
7178
client_side_statement_type = ClientSideStatementType.BEGIN
7279
elif RE_RUN_BATCH.match(query):
7380
client_side_statement_type = ClientSideStatementType.RUN_BATCH

owlbot.py

Lines changed: 3 additions & 217 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def get_staging_dirs(
8585
excludes=[
8686
"google/cloud/spanner/**",
8787
"*.*",
88+
"noxfile.py",
8889
"docs/index.rst",
8990
"google/cloud/spanner_v1/__init__.py",
9091
"**/gapic_version.py",
@@ -102,7 +103,7 @@ def get_staging_dirs(
102103
)
103104
s.move(
104105
library,
105-
excludes=["google/cloud/spanner_admin_instance/**", "*.*", "docs/index.rst", "**/gapic_version.py", "testing/constraints-3.7.txt",],
106+
excludes=["google/cloud/spanner_admin_instance/**", "*.*", "docs/index.rst", "noxfile.py", "**/gapic_version.py", "testing/constraints-3.7.txt",],
106107
)
107108

108109
for library in get_staging_dirs(
@@ -115,7 +116,7 @@ def get_staging_dirs(
115116
)
116117
s.move(
117118
library,
118-
excludes=["google/cloud/spanner_admin_database/**", "*.*", "docs/index.rst", "**/gapic_version.py", "testing/constraints-3.7.txt",],
119+
excludes=["google/cloud/spanner_admin_database/**", "*.*", "docs/index.rst", "noxfile.py", "**/gapic_version.py", "testing/constraints-3.7.txt",],
119120
)
120121

121122
s.remove_staging_dirs()
@@ -161,219 +162,4 @@ def get_staging_dirs(
161162

162163
python.py_samples()
163164

164-
# ----------------------------------------------------------------------------
165-
# Customize noxfile.py
166-
# ----------------------------------------------------------------------------
167-
168-
169-
def place_before(path, text, *before_text, escape=None):
170-
replacement = "\n".join(before_text) + "\n" + text
171-
if escape:
172-
for c in escape:
173-
text = text.replace(c, "\\" + c)
174-
s.replace([path], text, replacement)
175-
176-
177-
open_telemetry_test = """
178-
# XXX Work around Kokoro image's older pip, which borks the OT install.
179-
session.run("pip", "install", "--upgrade", "pip")
180-
constraints_path = str(
181-
CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
182-
)
183-
session.install("-e", ".[tracing]", "-c", constraints_path)
184-
# XXX: Dump installed versions to debug OT issue
185-
session.run("pip", "list")
186-
187-
# Run py.test against the unit tests with OpenTelemetry.
188-
session.run(
189-
"py.test",
190-
"--quiet",
191-
"--cov=google.cloud.spanner",
192-
"--cov=google.cloud",
193-
"--cov=tests.unit",
194-
"--cov-append",
195-
"--cov-config=.coveragerc",
196-
"--cov-report=",
197-
"--cov-fail-under=0",
198-
os.path.join("tests", "unit"),
199-
*session.posargs,
200-
)
201-
"""
202-
203-
place_before(
204-
"noxfile.py",
205-
"@nox.session(python=UNIT_TEST_PYTHON_VERSIONS)",
206-
open_telemetry_test,
207-
escape="()",
208-
)
209-
210-
skip_tests_if_env_var_not_set = """# Sanity check: Only run tests if the environment variable is set.
211-
if not os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", "") and not os.environ.get(
212-
"SPANNER_EMULATOR_HOST", ""
213-
):
214-
session.skip(
215-
"Credentials or emulator host must be set via environment variable"
216-
)
217-
# If POSTGRESQL tests and Emulator, skip the tests
218-
if os.environ.get("SPANNER_EMULATOR_HOST") and database_dialect == "POSTGRESQL":
219-
session.skip("Postgresql is not supported by Emulator yet.")
220-
"""
221-
222-
place_before(
223-
"noxfile.py",
224-
"# Install pyopenssl for mTLS testing.",
225-
skip_tests_if_env_var_not_set,
226-
escape="()",
227-
)
228-
229-
s.replace(
230-
"noxfile.py",
231-
r"""session.install\("-e", "."\)""",
232-
"""session.install("-e", ".[tracing]")""",
233-
)
234-
235-
# Apply manual changes from PR https://github.com/googleapis/python-spanner/pull/759
236-
s.replace(
237-
"noxfile.py",
238-
"""@nox.session\(python=SYSTEM_TEST_PYTHON_VERSIONS\)
239-
def system\(session\):""",
240-
"""@nox.session(python=SYSTEM_TEST_PYTHON_VERSIONS)
241-
@nox.parametrize(
242-
"protobuf_implementation,database_dialect",
243-
[
244-
("python", "GOOGLE_STANDARD_SQL"),
245-
("python", "POSTGRESQL"),
246-
("upb", "GOOGLE_STANDARD_SQL"),
247-
("upb", "POSTGRESQL"),
248-
("cpp", "GOOGLE_STANDARD_SQL"),
249-
("cpp", "POSTGRESQL"),
250-
],
251-
)
252-
def system(session, protobuf_implementation, database_dialect):""",
253-
)
254-
255-
s.replace(
256-
"noxfile.py",
257-
"""\*session.posargs,
258-
\)""",
259-
"""*session.posargs,
260-
env={
261-
"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation,
262-
"SPANNER_DATABASE_DIALECT": database_dialect,
263-
"SKIP_BACKUP_TESTS": "true",
264-
},
265-
)""",
266-
)
267-
268-
s.replace("noxfile.py",
269-
"""env={
270-
"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation,
271-
},""",
272-
"""env={
273-
"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation,
274-
"SPANNER_DATABASE_DIALECT": database_dialect,
275-
"SKIP_BACKUP_TESTS": "true",
276-
},""",
277-
)
278-
279-
s.replace("noxfile.py",
280-
"""session.run\(
281-
"py.test",
282-
"tests/unit",
283-
env={
284-
"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation,
285-
},
286-
\)""",
287-
"""session.run(
288-
"py.test",
289-
"tests/unit",
290-
env={
291-
"PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION": protobuf_implementation,
292-
"SPANNER_DATABASE_DIALECT": database_dialect,
293-
"SKIP_BACKUP_TESTS": "true",
294-
},
295-
)""",
296-
)
297-
298-
s.replace(
299-
"noxfile.py",
300-
"""\@nox.session\(python="3.13"\)
301-
\@nox.parametrize\(
302-
"protobuf_implementation",
303-
\[ "python", "upb", "cpp" \],
304-
\)
305-
def prerelease_deps\(session, protobuf_implementation\):""",
306-
"""@nox.session(python="3.13")
307-
@nox.parametrize(
308-
"protobuf_implementation,database_dialect",
309-
[
310-
("python", "GOOGLE_STANDARD_SQL"),
311-
("python", "POSTGRESQL"),
312-
("upb", "GOOGLE_STANDARD_SQL"),
313-
("upb", "POSTGRESQL"),
314-
("cpp", "GOOGLE_STANDARD_SQL"),
315-
("cpp", "POSTGRESQL"),
316-
],
317-
)
318-
def prerelease_deps(session, protobuf_implementation, database_dialect):""",
319-
)
320-
321-
322-
mockserver_test = """
323-
@nox.session(python=DEFAULT_MOCK_SERVER_TESTS_PYTHON_VERSION)
324-
def mockserver(session):
325-
# Install all test dependencies, then install this package in-place.
326-
327-
constraints_path = str(
328-
CURRENT_DIRECTORY / "testing" / f"constraints-{session.python}.txt"
329-
)
330-
# install_unittest_dependencies(session, "-c", constraints_path)
331-
standard_deps = UNIT_TEST_STANDARD_DEPENDENCIES + UNIT_TEST_DEPENDENCIES
332-
session.install(*standard_deps, "-c", constraints_path)
333-
session.install("-e", ".", "-c", constraints_path)
334-
335-
# Run py.test against the mockserver tests.
336-
session.run(
337-
"py.test",
338-
"--quiet",
339-
f"--junitxml=unit_{session.python}_sponge_log.xml",
340-
"--cov=google",
341-
"--cov=tests/unit",
342-
"--cov-append",
343-
"--cov-config=.coveragerc",
344-
"--cov-report=",
345-
"--cov-fail-under=0",
346-
os.path.join("tests", "mockserver_tests"),
347-
*session.posargs,
348-
)
349-
350-
"""
351-
352-
place_before(
353-
"noxfile.py",
354-
"def install_systemtest_dependencies(session, *constraints):",
355-
mockserver_test,
356-
escape="()_*:",
357-
)
358-
359-
s.replace(
360-
"noxfile.py",
361-
"install_systemtest_dependencies\(session, \"-c\", constraints_path\)",
362-
"""install_systemtest_dependencies(session, "-c", constraints_path)
363-
364-
# TODO(https://github.com/googleapis/synthtool/issues/1976):
365-
# Remove the 'cpp' implementation once support for Protobuf 3.x is dropped.
366-
# The 'cpp' implementation requires Protobuf<4.
367-
if protobuf_implementation == "cpp":
368-
session.install("protobuf<4")
369-
"""
370-
)
371-
372-
place_before(
373-
"noxfile.py",
374-
"UNIT_TEST_PYTHON_VERSIONS: List[str] = [",
375-
'DEFAULT_MOCK_SERVER_TESTS_PYTHON_VERSION = "3.12"',
376-
escape="[]",
377-
)
378-
379165
s.shell.run(["nox", "-s", "blacken"], hide_output=False)

tests/mockserver_tests/test_dbapi_isolation_level.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from google.api_core.exceptions import Unknown
1516
from google.cloud.spanner_dbapi import Connection
1617
from google.cloud.spanner_v1 import (
1718
BeginTransactionRequest,
@@ -117,3 +118,33 @@ def test_transaction_isolation_level(self):
117118
self.assertEqual(1, len(begin_requests))
118119
self.assertEqual(begin_requests[0].options.isolation_level, level)
119120
MockServerTestBase.spanner_service.clear_requests()
121+
122+
def test_begin_isolation_level(self):
123+
connection = Connection(self.instance, self.database)
124+
for level in [
125+
TransactionOptions.IsolationLevel.REPEATABLE_READ,
126+
TransactionOptions.IsolationLevel.SERIALIZABLE,
127+
]:
128+
isolation_level_name = level.name.replace("_", " ")
129+
with connection.cursor() as cursor:
130+
cursor.execute(f"begin isolation level {isolation_level_name}")
131+
cursor.execute(
132+
"insert into singers (id, name) values (1, 'Some Singer')"
133+
)
134+
self.assertEqual(1, cursor.rowcount)
135+
connection.commit()
136+
begin_requests = list(
137+
filter(
138+
lambda msg: isinstance(msg, BeginTransactionRequest),
139+
self.spanner_service.requests,
140+
)
141+
)
142+
self.assertEqual(1, len(begin_requests))
143+
self.assertEqual(begin_requests[0].options.isolation_level, level)
144+
MockServerTestBase.spanner_service.clear_requests()
145+
146+
def test_begin_invalid_isolation_level(self):
147+
connection = Connection(self.instance, self.database)
148+
with connection.cursor() as cursor:
149+
with self.assertRaises(Unknown):
150+
cursor.execute("begin isolation level does_not_exist")

0 commit comments

Comments
 (0)