Skip to content

Commit 1d95d61

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 1d95d61

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)