Skip to content

Commit 5832882

Browse files
committed
feat(sqlalchemy-spanner): wire timeout execution option through to DBAPI Connection.timeout
Add timeout handling to SpannerExecutionContext.pre_exec() and reset_connection() so that users can set a per-statement gRPC deadline via execution_options(timeout=N). Depends on googleapis/python-spanner#1534 for the DBAPI Connection.timeout property. Fixes #16467
1 parent 0f8d933 commit 5832882

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def reset_connection(dbapi_conn, connection_record, reset_state=None):
7272

7373
dbapi_conn.staleness = None
7474
dbapi_conn.read_only = False
75+
dbapi_conn.timeout = None
7576

7677

7778
# register a method to get a single value of a JSON object
@@ -217,6 +218,10 @@ def pre_exec(self):
217218
if request_tag:
218219
self.cursor.request_tag = request_tag
219220

221+
timeout = self.execution_options.get("timeout")
222+
if timeout is not None:
223+
self._dbapi_connection.connection.timeout = timeout
224+
220225
ignore_transaction_warnings = self.execution_options.get(
221226
"ignore_transaction_warnings"
222227
)
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2024 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+
"""Unit tests for SpannerExecutionContext and reset_connection."""
16+
17+
import unittest
18+
from unittest import mock
19+
20+
from google.cloud import spanner_dbapi
21+
from google.cloud.sqlalchemy_spanner.sqlalchemy_spanner import reset_connection
22+
23+
24+
class ResetConnectionTest(unittest.TestCase):
25+
def test_reset_connection_clears_timeout(self):
26+
dbapi_conn = mock.MagicMock(spec=spanner_dbapi.Connection)
27+
dbapi_conn.inside_transaction = False
28+
29+
reset_connection(dbapi_conn, connection_record=None)
30+
31+
assert dbapi_conn.staleness is None
32+
assert dbapi_conn.read_only is False
33+
assert dbapi_conn.timeout is None
34+
35+
def test_reset_connection_with_wrapper(self):
36+
inner_conn = mock.MagicMock(spec=spanner_dbapi.Connection)
37+
inner_conn.inside_transaction = False
38+
wrapper = mock.MagicMock()
39+
wrapper.connection = inner_conn
40+
41+
reset_connection(wrapper, connection_record=None)
42+
43+
assert inner_conn.staleness is None
44+
assert inner_conn.read_only is False
45+
assert inner_conn.timeout is None
46+
47+
48+
class SpannerExecutionContextPreExecTest(unittest.TestCase):
49+
def _make_context(self, execution_options):
50+
from google.cloud.sqlalchemy_spanner.sqlalchemy_spanner import (
51+
SpannerExecutionContext,
52+
)
53+
54+
ctx = SpannerExecutionContext.__new__(SpannerExecutionContext)
55+
ctx.execution_options = execution_options
56+
57+
dbapi_conn = mock.MagicMock()
58+
dbapi_conn.connection = mock.MagicMock()
59+
ctx._dbapi_connection = dbapi_conn
60+
ctx.cursor = mock.MagicMock()
61+
62+
return ctx
63+
64+
@mock.patch(
65+
"google.cloud.sqlalchemy_spanner.sqlalchemy_spanner.DefaultExecutionContext.pre_exec"
66+
)
67+
def test_pre_exec_sets_timeout(self, mock_super_pre_exec):
68+
ctx = self._make_context({"timeout": 60})
69+
ctx.pre_exec()
70+
71+
assert ctx._dbapi_connection.connection.timeout == 60
72+
73+
@mock.patch(
74+
"google.cloud.sqlalchemy_spanner.sqlalchemy_spanner.DefaultExecutionContext.pre_exec"
75+
)
76+
def test_pre_exec_no_timeout_leaves_connection_unchanged(self, mock_super_pre_exec):
77+
ctx = self._make_context({})
78+
79+
conn = ctx._dbapi_connection.connection
80+
conn._mock_children.clear()
81+
82+
ctx.pre_exec()
83+
84+
set_attrs = {
85+
name
86+
for name, _ in conn._mock_children.items()
87+
if not name.startswith("_")
88+
}
89+
assert "timeout" not in set_attrs
90+
91+
@mock.patch(
92+
"google.cloud.sqlalchemy_spanner.sqlalchemy_spanner.DefaultExecutionContext.pre_exec"
93+
)
94+
def test_pre_exec_timeout_with_other_options(self, mock_super_pre_exec):
95+
ctx = self._make_context(
96+
{"timeout": 30, "read_only": True, "request_priority": 2}
97+
)
98+
ctx.pre_exec()
99+
100+
assert ctx._dbapi_connection.connection.timeout == 30
101+
assert ctx._dbapi_connection.connection.read_only is True
102+
assert ctx._dbapi_connection.connection.request_priority == 2

0 commit comments

Comments
 (0)