chore: test minimum dependencies in python 3.7 by parthea · Pull Request #740 · googleapis/python-spanner
This was referenced
Feb 26, 2026olavloite added a commit that referenced this pull request
Mar 13, 2026Fixes googleapis/google-cloud-python#15871 ## Summary `Connection.transaction_checkout()` currently calls `Transaction.begin()` explicitly before the first query, which sends a standalone `BeginTransaction` gRPC RPC. This is unnecessary because the `Transaction` class already supports **inline begin** — piggybacking `BeginTransaction` onto the first `ExecuteSql`/`ExecuteBatchDml` request via `TransactionSelector(begin=...)`. This PR removes the explicit `begin()` call, letting the existing inline begin logic in `execute_sql()`, `execute_update()`, and `batch_update()` handle transaction creation. This saves **one gRPC round-trip per transaction** (~16ms measured on the emulator). ### What changed **`google/cloud/spanner_dbapi/connection.py`** — `transaction_checkout()`: - Removed `self._transaction.begin()` on [L413](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_dbapi/connection.py#L413) - The transaction is now returned with `_transaction_id=None` - Updated docstring to explain the inline begin behavior **`tests/unit/spanner_dbapi/test_connection.py`**: - Added `test_transaction_checkout_does_not_call_begin` to assert `begin()` is not called **`tests/mockserver_tests/test_dbapi_inline_begin.py`** (new): - 9 mockserver tests verifying inline begin behavior for read-write transactions - Covers: no `BeginTransactionRequest` sent, first `ExecuteSqlRequest` uses `TransactionSelector(begin=...)`, transaction ID reuse on second statement, rollback, read-only unaffected, retry after abort **`tests/mockserver_tests/test_tags.py`**: - Updated 4 read-write tag tests: removed `BeginTransactionRequest` from expected RPC sequences, adjusted tag index offsets **`tests/mockserver_tests/test_dbapi_isolation_level.py`**: - Updated 4 isolation level tests: verify isolation level on `ExecuteSqlRequest.transaction.begin.isolation_level` instead of `BeginTransactionRequest.options.isolation_level` ### Why this is safe The inline begin code path already exists and is battle-tested — `Session.run_in_transaction()` creates a `Transaction` without calling `begin()` and relies on the same inline begin logic ([session.py L566](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_v1/session.py#L566)). Specific safety analysis: 1. **`transaction_checkout()` callers always execute SQL immediately**: It's only called from `run_statement()` → `execute_sql()` and `batch_dml_executor` → `batch_update()`. Both set `_transaction_id` via inline begin before any commit/rollback path. 2. **`execute_sql`/`execute_update`/`batch_update` handle `_transaction_id is None`**: They acquire a lock, use `_make_txn_selector()` which returns `TransactionSelector(begin=...)`, and store the returned `_transaction_id` ([transaction.py L612-L623](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_v1/transaction.py#L612-L623)). 3. **`rollback()` handles `_transaction_id is None`**: Skips the RPC — correct when no server-side transaction exists ([transaction.py L163](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_v1/transaction.py#L163)). 4. **`commit()` handles `_transaction_id is None`**: Falls back to `_begin_mutations_only_transaction()` for mutation-only transactions ([transaction.py L263-L267](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_v1/transaction.py#L263-L267)). 5. **Retry mechanism is compatible**: `_set_connection_for_retry()` resets `_spanner_transaction_started=False`, so replayed statements go through `transaction_checkout()` again, create a fresh `Transaction`, and use inline begin. ### PEP 249 conformance This change is fully conformant with [PEP 249 (DB-API 2.0)](https://peps.python.org/pep-0249/). The spec does not define a `begin()` method — transactions are implicit. The PEP author [clarified on the DB-SIG mailing list](https://mail.python.org/pipermail/db-sig/2010-September/005645.html) that *"transactions start implicitly after you connect and after you call `.commit()` or `.rollback()`"*, and the mechanism by which the driver starts the server-side transaction is an implementation detail. Deferring the server-side begin to the first SQL execution (as psycopg2 and other mature DB-API drivers do) is the standard approach. The observable transactional semantics — atomicity between `commit()`/`rollback()` calls — are unchanged. ### Performance impact Before (4 RPCs per read-write transaction): ``` BeginTransaction → ExecuteSql (read) → ExecuteSql (write) → Commit ``` After (3 RPCs per read-write transaction): ``` ExecuteSql (read, with inline begin) → ExecuteSql (write) → Commit ``` Measured ~16ms savings per transaction on the Spanner emulator. ### Context This optimization was identified while profiling SQLAlchemy/DBAPI performance against Spanner. The DBAPI was created ~2 years before inline begin support was added to the Python client library (inline begin landed in [PR #740](#740), Dec 2022). The `run_in_transaction` path was updated to use inline begin, but `transaction_checkout` was not. ## Test plan - [x] All 198 DBAPI unit tests pass (`tests/unit/spanner_dbapi/`) - [x] New unit test verifies `begin()` is not called by `transaction_checkout()` - [x] 9 new mockserver tests verify inline begin RPC behavior (`tests/mockserver_tests/test_dbapi_inline_begin.py`) - [x] 8 existing mockserver tests updated for inline begin expectations (`test_tags.py`, `test_dbapi_isolation_level.py`) - [x] All 53 DBAPI system tests pass against Spanner emulator (`tests/system/test_dbapi.py`) - [ ] CI system tests (will run automatically) --------- Co-authored-by: rahul2393 <irahul@google.com> Co-authored-by: Knut Olav Løite <koloite@gmail.com>
chalmerlowe pushed a commit to googleapis/google-cloud-python that referenced this pull request
Mar 23, 2026Fixes #15871 ## Summary `Connection.transaction_checkout()` currently calls `Transaction.begin()` explicitly before the first query, which sends a standalone `BeginTransaction` gRPC RPC. This is unnecessary because the `Transaction` class already supports **inline begin** — piggybacking `BeginTransaction` onto the first `ExecuteSql`/`ExecuteBatchDml` request via `TransactionSelector(begin=...)`. This PR removes the explicit `begin()` call, letting the existing inline begin logic in `execute_sql()`, `execute_update()`, and `batch_update()` handle transaction creation. This saves **one gRPC round-trip per transaction** (~16ms measured on the emulator). ### What changed **`google/cloud/spanner_dbapi/connection.py`** — `transaction_checkout()`: - Removed `self._transaction.begin()` on [L413](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_dbapi/connection.py#L413) - The transaction is now returned with `_transaction_id=None` - Updated docstring to explain the inline begin behavior **`tests/unit/spanner_dbapi/test_connection.py`**: - Added `test_transaction_checkout_does_not_call_begin` to assert `begin()` is not called **`tests/mockserver_tests/test_dbapi_inline_begin.py`** (new): - 9 mockserver tests verifying inline begin behavior for read-write transactions - Covers: no `BeginTransactionRequest` sent, first `ExecuteSqlRequest` uses `TransactionSelector(begin=...)`, transaction ID reuse on second statement, rollback, read-only unaffected, retry after abort **`tests/mockserver_tests/test_tags.py`**: - Updated 4 read-write tag tests: removed `BeginTransactionRequest` from expected RPC sequences, adjusted tag index offsets **`tests/mockserver_tests/test_dbapi_isolation_level.py`**: - Updated 4 isolation level tests: verify isolation level on `ExecuteSqlRequest.transaction.begin.isolation_level` instead of `BeginTransactionRequest.options.isolation_level` ### Why this is safe The inline begin code path already exists and is battle-tested — `Session.run_in_transaction()` creates a `Transaction` without calling `begin()` and relies on the same inline begin logic ([session.py L566](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_v1/session.py#L566)). Specific safety analysis: 1. **`transaction_checkout()` callers always execute SQL immediately**: It's only called from `run_statement()` → `execute_sql()` and `batch_dml_executor` → `batch_update()`. Both set `_transaction_id` via inline begin before any commit/rollback path. 2. **`execute_sql`/`execute_update`/`batch_update` handle `_transaction_id is None`**: They acquire a lock, use `_make_txn_selector()` which returns `TransactionSelector(begin=...)`, and store the returned `_transaction_id` ([transaction.py L612-L623](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_v1/transaction.py#L612-L623)). 3. **`rollback()` handles `_transaction_id is None`**: Skips the RPC — correct when no server-side transaction exists ([transaction.py L163](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_v1/transaction.py#L163)). 4. **`commit()` handles `_transaction_id is None`**: Falls back to `_begin_mutations_only_transaction()` for mutation-only transactions ([transaction.py L263-L267](https://github.com/googleapis/python-spanner/blob/v3.63.0/google/cloud/spanner_v1/transaction.py#L263-L267)). 5. **Retry mechanism is compatible**: `_set_connection_for_retry()` resets `_spanner_transaction_started=False`, so replayed statements go through `transaction_checkout()` again, create a fresh `Transaction`, and use inline begin. ### PEP 249 conformance This change is fully conformant with [PEP 249 (DB-API 2.0)](https://peps.python.org/pep-0249/). The spec does not define a `begin()` method — transactions are implicit. The PEP author [clarified on the DB-SIG mailing list](https://mail.python.org/pipermail/db-sig/2010-September/005645.html) that *"transactions start implicitly after you connect and after you call `.commit()` or `.rollback()`"*, and the mechanism by which the driver starts the server-side transaction is an implementation detail. Deferring the server-side begin to the first SQL execution (as psycopg2 and other mature DB-API drivers do) is the standard approach. The observable transactional semantics — atomicity between `commit()`/`rollback()` calls — are unchanged. ### Performance impact Before (4 RPCs per read-write transaction): ``` BeginTransaction → ExecuteSql (read) → ExecuteSql (write) → Commit ``` After (3 RPCs per read-write transaction): ``` ExecuteSql (read, with inline begin) → ExecuteSql (write) → Commit ``` Measured ~16ms savings per transaction on the Spanner emulator. ### Context This optimization was identified while profiling SQLAlchemy/DBAPI performance against Spanner. The DBAPI was created ~2 years before inline begin support was added to the Python client library (inline begin landed in [PR googleapis/python-spanner#740](googleapis/python-spanner#740), Dec 2022). The `run_in_transaction` path was updated to use inline begin, but `transaction_checkout` was not. ## Test plan - [x] All 198 DBAPI unit tests pass (`tests/unit/spanner_dbapi/`) - [x] New unit test verifies `begin()` is not called by `transaction_checkout()` - [x] 9 new mockserver tests verify inline begin RPC behavior (`tests/mockserver_tests/test_dbapi_inline_begin.py`) - [x] 8 existing mockserver tests updated for inline begin expectations (`test_tags.py`, `test_dbapi_isolation_level.py`) - [x] All 53 DBAPI system tests pass against Spanner emulator (`tests/system/test_dbapi.py`) - [ ] CI system tests (will run automatically) --------- Co-authored-by: rahul2393 <irahul@google.com> Co-authored-by: Knut Olav Løite <koloite@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters