diff --git a/packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py b/packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py index ee8e72eb5665..43433690d41f 100644 --- a/packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py +++ b/packages/sqlalchemy-spanner/google/cloud/sqlalchemy_spanner/sqlalchemy_spanner.py @@ -228,6 +228,10 @@ def pre_exec(self): if request_tag: self.cursor.request_tag = request_tag + timeout = self.execution_options.get("timeout") + if timeout is not None: + self.cursor.timeout = timeout + ignore_transaction_warnings = self.execution_options.get( "ignore_transaction_warnings" ) diff --git a/packages/sqlalchemy-spanner/noxfile.py b/packages/sqlalchemy-spanner/noxfile.py index 56884d95296d..d083e8eddcde 100644 --- a/packages/sqlalchemy-spanner/noxfile.py +++ b/packages/sqlalchemy-spanner/noxfile.py @@ -120,10 +120,11 @@ class = StreamHandler SQLALCHEMY_14_DEPENDENCIES = [ "sqlalchemy>=1.4,<2.0", + "alembic<1.20", ] SQLALCHEMY_20_DEPENDENCIES = [ - "sqlalchemy>=2.0", + "sqlalchemy>=2.0,<2.1", ] UNIT_TEST_PYTHON_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"] diff --git a/packages/sqlalchemy-spanner/tests/test_suite_20.py b/packages/sqlalchemy-spanner/tests/test_suite_20.py index 6c975004a8c4..87e8c2ef120b 100644 --- a/packages/sqlalchemy-spanner/tests/test_suite_20.py +++ b/packages/sqlalchemy-spanner/tests/test_suite_20.py @@ -78,7 +78,11 @@ LongNameBlowoutTest as _LongNameBlowoutTest, ) from sqlalchemy.testing.suite.test_ddl import TableDDLTest as _TableDDLTest -from sqlalchemy.testing.suite.test_deprecations import * # noqa: F401, F403 + +try: + from sqlalchemy.testing.suite.test_deprecations import * # noqa: F401, F403 +except ImportError: + pass from sqlalchemy.testing.suite.test_dialect import * # noqa: F401, F403 from sqlalchemy.testing.suite.test_dialect import ( DifficultParametersTest as _DifficultParametersTest, diff --git a/packages/sqlalchemy-spanner/tests/unit/test_timeout.py b/packages/sqlalchemy-spanner/tests/unit/test_timeout.py new file mode 100644 index 000000000000..984e892b3ae6 --- /dev/null +++ b/packages/sqlalchemy-spanner/tests/unit/test_timeout.py @@ -0,0 +1,70 @@ +# Copyright 2026 Google LLC All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +from sqlalchemy.testing import eq_ +from sqlalchemy.testing.plugin.plugin_base import fixtures + +from google.cloud import spanner_dbapi +from google.cloud.sqlalchemy_spanner.sqlalchemy_spanner import ( + SpannerDialect, + SpannerExecutionContext, + reset_connection, +) + + +class SqlAlchemyTimeoutTest(fixtures.TestBase): + def test_reset_connection_preserves_default_timeout(self): + dbapi_conn = mock.MagicMock(spec=spanner_dbapi.Connection) + dbapi_conn.timeout = 30.0 + dbapi_conn.inside_transaction = False + + reset_connection(dbapi_conn, None) + + eq_(dbapi_conn.timeout, 30.0) + + def test_pre_exec_sets_cursor_timeout(self): + context = SpannerExecutionContext() + context.execution_options = {"timeout": 45.0} + context.cursor = mock.MagicMock() + context.cursor.timeout = None + + dbapi_conn = mock.MagicMock(spec=spanner_dbapi.Connection) + dbapi_conn.timeout = 25.0 + context._dbapi_connection = mock.MagicMock() + context._dbapi_connection.connection = dbapi_conn + + context.pre_exec() + + eq_(context.cursor.timeout, 45.0) + eq_(dbapi_conn.timeout, 25.0) + + def test_query_without_timeout_does_not_alter_cursor_or_connection_timeout(self): + dialect = SpannerDialect() + dbapi_conn = mock.MagicMock(spec=spanner_dbapi.Connection) + dbapi_conn.timeout = 25.0 + + context = SpannerExecutionContext() + context.dialect = dialect + context.execution_options = {} + context.cursor = mock.MagicMock() + context.cursor.timeout = None + context._dbapi_connection = mock.MagicMock() + context._dbapi_connection.connection = dbapi_conn + + context.pre_exec() + + eq_(context.cursor.timeout, None) + eq_(dbapi_conn.timeout, 25.0)