Skip to content

feat(dbapi): wire timeout parameter through Connection to execute_sql#1535

Open
waiho-gumloop wants to merge 1 commit intogoogleapis:mainfrom
waiho-gumloop:feat/dbapi-timeout-parameter
Open

feat(dbapi): wire timeout parameter through Connection to execute_sql#1535
waiho-gumloop wants to merge 1 commit intogoogleapis:mainfrom
waiho-gumloop:feat/dbapi-timeout-parameter

Conversation

@waiho-gumloop
Copy link
Contributor

@waiho-gumloop waiho-gumloop commented Mar 27, 2026

Summary

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.

Fixes #1534

Background

_SnapshotBase.execute_sql() accepts a timeout parameter that controls the gRPC deadline for the ExecuteStreamingSql RPC. When not provided, it defaults to gapic_v1.method.DEFAULT, which resolves to default_timeout=3600.0 in the transport layer.

The DBAPI calls execute_sql() in three locations, none of which pass timeout=:

  1. Snapshot readscursor._handle_DQL_with_snapshot() calls snapshot.execute_sql(sql, params, param_types, request_options=...) without timeout=
  2. Transaction reads/writesconnection.run_statement() calls transaction.execute_sql(sql, params, param_types=..., request_options=...) without timeout=
  3. Autocommit DMLcursor._do_execute_update_in_autocommit() calls transaction.execute_sql(sql, params=..., param_types=..., last_statement=True) without timeout=

This means DBAPI consumers (SQLAlchemy, Django, raw DBAPI) cannot control the gRPC deadline for individual statements — all queries use the 3600-second default.

Timeline

Date Commit Event
Nov 2018 9b7fcd6 (PR #6536) timeout= parameter added to _SnapshotBase.execute_sql() in the Spanner client
Nov 2020 2493fa1 (PR #160) DBAPI created — cursor._handle_DQL_with_snapshot() calls execute_sql() without timeout=
Nov 2020 d59d502 (PR #168) connection.run_statement() added — calls execute_sql() without timeout=
Mar 2021 1a7c9d2 (PR #278) timeout= expanded to read(), partition_read(), partition_query() in the client
Oct 2021 cd3b950 (PR #475) _handle_DQL_with_snapshot() extracted as separate method — still no timeout=
Oct 2022 ab768e4 (PR #838) request_options added to _handle_DQL_with_snapshot()timeout= not added
Jan 2025 ee9662f (PR #1262) request_tag/transaction_tag added to cursor/connection — timeout= not added

Other execution parameters (request_options, request_priority, transaction_tag, request_tag) were each wired through the DBAPI incrementally. The timeout parameter was not included in any of these additions.

Changes

connection.py

  • Add self._timeout = None to __init__
  • Add timeout property and setter
  • Pass timeout=self._timeout in run_statement() when set

cursor.py

  • Pass timeout=self.connection._timeout in _handle_DQL_with_snapshot() when set
  • Pass timeout=self.connection._timeout in _do_execute_update_in_autocommit() when set

When timeout is None (the default), timeout= is not passed, preserving the existing behavior of using gapic_v1.method.DEFAULT.

Usage

from google.cloud.spanner_dbapi import connect

conn = connect(instance_id, database_id, project=project)
conn.timeout = 60  # 60-second gRPC deadline for subsequent statements

cursor = conn.cursor()
cursor.execute("SELECT * FROM my_table")

Tests

Added 7 unit tests:

  • test_timeout_default_none — verifies default is None
  • test_timeout_property — verifies getter/setter
  • test_timeout_passed_to_run_statement — verifies timeout= is passed in run_statement()
  • test_timeout_not_passed_when_none — verifies timeout= is omitted when None
  • test_do_execute_update_with_timeout — verifies timeout= in autocommit DML path
  • test_handle_DQL_with_snapshot_timeout — verifies timeout= in snapshot read path
  • test_handle_DQL_with_snapshot_no_timeout — verifies omission in snapshot read path

All 205 existing DBAPI tests continue to pass.

Related

@waiho-gumloop waiho-gumloop requested review from a team as code owners March 27, 2026 01:48
@product-auto-label product-auto-label bot added size: m Pull request size is medium. api: spanner Issues related to the googleapis/python-spanner API. labels Mar 27, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a timeout property to the Connection class in the Spanner DB-API, allowing users to set a gRPC deadline for SQL operations. The timeout value is propagated to execute_sql calls in run_statement, _do_execute_update_in_autocommit, and _handle_DQL_with_snapshot. Corresponding unit tests have been added to verify the property behavior and its correct application during execution. Feedback suggests refactoring the execute_sql call in _handle_DQL_with_snapshot to use a consistent keyword argument pattern for param_types, matching other updated call sites.

Comment on lines 549 to 557
kwargs = dict(request_options=self.request_options)
if self.connection._timeout is not None:
kwargs["timeout"] = self.connection._timeout
self._result_set = snapshot.execute_sql(
sql,
params,
get_param_types(params),
request_options=self.request_options,
**kwargs,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency with the changes in run_statement and _do_execute_update_in_autocommit, consider including param_types in the kwargs dictionary instead of passing it as a positional argument. This would make all three modified call sites for execute_sql follow the same pattern, improving maintainability.

Suggested change
kwargs = dict(request_options=self.request_options)
if self.connection._timeout is not None:
kwargs["timeout"] = self.connection._timeout
self._result_set = snapshot.execute_sql(
sql,
params,
get_param_types(params),
request_options=self.request_options,
**kwargs,
)
kwargs = dict(
param_types=get_param_types(params),
request_options=self.request_options,
)
if self.connection._timeout is not None:
kwargs["timeout"] = self.connection._timeout
self._result_set = snapshot.execute_sql(
sql,
params,
**kwargs,
)

The DBAPI layer calls _SnapshotBase.execute_sql() in three code paths
(snapshot reads, transaction reads/writes, autocommit DML) but never
passes the timeout= argument. This causes all queries to use the gRPC
default timeout of 3600 seconds.

Add a timeout property to Connection and pass it through to
execute_sql() in cursor._handle_DQL_with_snapshot(),
cursor._do_execute_update_in_autocommit(), and
connection.run_statement().

Fixes googleapis#1534
@waiho-gumloop waiho-gumloop force-pushed the feat/dbapi-timeout-parameter branch from b73fa31 to f93bed6 Compare March 27, 2026 02:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api: spanner Issues related to the googleapis/python-spanner API. size: m Pull request size is medium.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(dbapi): wire timeout parameter through to execute_sql in cursor and connection

2 participants