Skip to content

Commit 861d7e9

Browse files
committed
feat(dbapi): wire timeout parameter through Connection to execute_sql
Add a timeout property to Connection and pass timeout= to execute_sql() in the three DBAPI code paths that currently omit it: snapshot reads, transaction statements, and autocommit DML. When timeout is None (the default), timeout= is not passed, preserving the existing behavior of using gapic_v1.method.DEFAULT (3600s). Fixes #16492
1 parent c3bd6c0 commit 861d7e9

File tree

4 files changed

+155
-7
lines changed

4 files changed

+155
-7
lines changed

packages/google-cloud-spanner/google/cloud/spanner_dbapi/connection.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ def __init__(self, instance, database=None, read_only=False, **kwargs):
112112
self._read_only = read_only
113113
self._staleness = None
114114
self.request_priority = None
115+
self._timeout = None
115116
self._transaction_begin_marked = False
116117
self._transaction_isolation_level = None
117118
# whether transaction started at Spanner. This means that we had
@@ -348,6 +349,30 @@ def staleness(self, value):
348349

349350
self._staleness = value
350351

352+
@property
353+
def timeout(self):
354+
"""Timeout in seconds for the next SQL operation on this connection.
355+
356+
When set, this value is passed as the ``timeout`` argument to
357+
``execute_sql`` calls on the underlying Spanner client, controlling
358+
the gRPC deadline for those calls.
359+
360+
Returns:
361+
Optional[float]: The timeout in seconds, or None to use the
362+
default gRPC timeout (3600s).
363+
"""
364+
return self._timeout
365+
366+
@timeout.setter
367+
def timeout(self, value):
368+
"""Set the timeout for subsequent SQL operations.
369+
370+
Args:
371+
value (Optional[float]): Timeout in seconds. Set to None to
372+
revert to the default gRPC timeout.
373+
"""
374+
self._timeout = value
375+
351376
def _session_checkout(self):
352377
"""Get a Cloud Spanner session from the pool.
353378
@@ -560,11 +585,16 @@ def run_statement(
560585
checksum of this statement results.
561586
"""
562587
transaction = self.transaction_checkout()
588+
kwargs = dict(
589+
param_types=statement.param_types,
590+
request_options=request_options or self.request_options,
591+
)
592+
if self._timeout is not None:
593+
kwargs["timeout"] = self._timeout
563594
return transaction.execute_sql(
564595
statement.sql,
565596
statement.params,
566-
param_types=statement.param_types,
567-
request_options=request_options or self.request_options,
597+
**kwargs,
568598
)
569599

570600
@check_not_closed

packages/google-cloud-spanner/google/cloud/spanner_dbapi/cursor.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -228,12 +228,17 @@ def _do_execute_update_in_autocommit(self, transaction, sql, params):
228228
"""This function should only be used in autocommit mode."""
229229
self.connection._transaction = transaction
230230
self.connection._snapshot = None
231-
self._result_set = transaction.execute_sql(
232-
sql,
231+
kwargs = dict(
233232
params=params,
234233
param_types=get_param_types(params),
235234
last_statement=True,
236235
)
236+
if self.connection._timeout is not None:
237+
kwargs["timeout"] = self.connection._timeout
238+
self._result_set = transaction.execute_sql(
239+
sql,
240+
**kwargs,
241+
)
237242
self._itr = PeekIterator(self._result_set)
238243
self._row_count = None
239244

@@ -542,11 +547,16 @@ def _fetch(self, cursor_statement_type, size=None):
542547
return rows
543548

544549
def _handle_DQL_with_snapshot(self, snapshot, sql, params):
550+
kwargs = dict(
551+
param_types=get_param_types(params),
552+
request_options=self.request_options,
553+
)
554+
if self.connection._timeout is not None:
555+
kwargs["timeout"] = self.connection._timeout
545556
self._result_set = snapshot.execute_sql(
546557
sql,
547558
params,
548-
get_param_types(params),
549-
request_options=self.request_options,
559+
**kwargs,
550560
)
551561
# Read the first element so that the StreamedResultSet can
552562
# return the metadata after a DQL statement.

packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_connection.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -838,6 +838,56 @@ def test_request_priority(self):
838838
sql, params, param_types=param_types, request_options=None
839839
)
840840

841+
def test_timeout_default_none(self):
842+
connection = self._make_connection()
843+
self.assertIsNone(connection.timeout)
844+
845+
def test_timeout_property(self):
846+
connection = self._make_connection()
847+
connection.timeout = 60
848+
self.assertEqual(connection.timeout, 60)
849+
850+
connection.timeout = None
851+
self.assertIsNone(connection.timeout)
852+
853+
def test_timeout_passed_to_run_statement(self):
854+
from google.cloud.spanner_dbapi.parsed_statement import Statement
855+
856+
sql = "SELECT 1"
857+
params = []
858+
param_types = {}
859+
860+
connection = self._make_connection()
861+
connection._spanner_transaction_started = True
862+
connection._transaction = mock.Mock()
863+
connection._transaction.execute_sql = mock.Mock()
864+
865+
connection.timeout = 60
866+
867+
connection.run_statement(Statement(sql, params, param_types))
868+
869+
connection._transaction.execute_sql.assert_called_with(
870+
sql, params, param_types=param_types, request_options=None, timeout=60
871+
)
872+
873+
def test_timeout_not_passed_when_none(self):
874+
from google.cloud.spanner_dbapi.parsed_statement import Statement
875+
876+
sql = "SELECT 1"
877+
params = []
878+
param_types = {}
879+
880+
connection = self._make_connection()
881+
connection._spanner_transaction_started = True
882+
connection._transaction = mock.Mock()
883+
connection._transaction.execute_sql = mock.Mock()
884+
885+
connection.run_statement(Statement(sql, params, param_types))
886+
887+
connection._transaction.execute_sql.assert_called_with(
888+
sql, params, param_types=param_types, request_options=None
889+
)
890+
841891
def test_custom_client_connection(self):
842892
from google.cloud.spanner_dbapi import connect
843893

packages/google-cloud-spanner/tests/unit/spanner_dbapi/test_cursor.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,64 @@ def test_do_execute_update(self):
123123
self.assertEqual(cursor._result_set, result_set)
124124
self.assertEqual(cursor.rowcount, 1234)
125125

126+
def test_do_execute_update_with_timeout(self):
127+
connection = self._make_connection(self.INSTANCE, self.DATABASE)
128+
connection._timeout = 30
129+
cursor = self._make_one(connection)
130+
transaction = mock.MagicMock()
131+
132+
cursor._do_execute_update_in_autocommit(
133+
transaction=transaction,
134+
sql="UPDATE t SET x=1 WHERE true",
135+
params={},
136+
)
137+
138+
transaction.execute_sql.assert_called_once_with(
139+
"UPDATE t SET x=1 WHERE true",
140+
params={},
141+
param_types={},
142+
last_statement=True,
143+
timeout=30,
144+
)
145+
146+
def test_handle_DQL_with_snapshot_timeout(self):
147+
connection = self._make_connection(self.INSTANCE, self.DATABASE)
148+
connection._timeout = 45
149+
cursor = self._make_one(connection)
150+
151+
snapshot = mock.MagicMock()
152+
result_set = mock.MagicMock()
153+
result_set.metadata.transaction.read_timestamp = None
154+
snapshot.execute_sql.return_value = result_set
155+
156+
cursor._handle_DQL_with_snapshot(snapshot, "SELECT 1", None)
157+
158+
snapshot.execute_sql.assert_called_once_with(
159+
"SELECT 1",
160+
None,
161+
param_types=None,
162+
request_options=None,
163+
timeout=45,
164+
)
165+
166+
def test_handle_DQL_with_snapshot_no_timeout(self):
167+
connection = self._make_connection(self.INSTANCE, self.DATABASE)
168+
cursor = self._make_one(connection)
169+
170+
snapshot = mock.MagicMock()
171+
result_set = mock.MagicMock()
172+
result_set.metadata.transaction.read_timestamp = None
173+
snapshot.execute_sql.return_value = result_set
174+
175+
cursor._handle_DQL_with_snapshot(snapshot, "SELECT 1", None)
176+
177+
snapshot.execute_sql.assert_called_once_with(
178+
"SELECT 1",
179+
None,
180+
param_types=None,
181+
request_options=None,
182+
)
183+
126184
def test_do_batch_update(self):
127185
from google.cloud.spanner_dbapi import connect
128186
from google.cloud.spanner_v1.param_types import INT64
@@ -953,7 +1011,7 @@ def test_handle_dql_priority(self, MockedPeekIterator):
9531011
self.assertEqual(cursor._itr, MockedPeekIterator())
9541012
self.assertEqual(cursor._row_count, None)
9551013
mock_snapshot.execute_sql.assert_called_with(
956-
sql, None, None, request_options=RequestOptions(priority=1)
1014+
sql, None, param_types=None, request_options=RequestOptions(priority=1)
9571015
)
9581016

9591017
def test_handle_dql_database_error(self):

0 commit comments

Comments
 (0)