-
Notifications
You must be signed in to change notification settings - Fork 105
Description
Environment details
- OS: macOS (also reproducible on Linux)
- Python version: 3.13.7
google-cloud-spannerversion: 3.63.0sqlalchemy-spannerversion: 1.17.2
Problem
Connection.transaction_checkout() calls Transaction.begin() explicitly before the first query, which sends a standalone BeginTransaction gRPC RPC. This adds an unnecessary round-trip to every read-write transaction initiated through the DBAPI/SQLAlchemy path.
The Transaction class already supports inline begin — piggybacking BeginTransaction onto the first ExecuteSql request via TransactionSelector(begin=...). This is what Session.run_in_transaction() uses (it creates a Transaction without calling begin()). But the DBAPI's transaction_checkout() bypasses this by eagerly calling begin(), which sets _transaction_id before the first query and prevents the inline begin path in _make_txn_selector().
Current behavior (4 RPCs)
BeginTransaction → ExecuteSql (read) → ExecuteSql (write) → Commit
Expected behavior (3 RPCs)
ExecuteSql (read, with inline begin) → ExecuteSql (write) → Commit
Performance impact
Measured ~16ms overhead per transaction on the Spanner emulator. This affects every transaction_scope write operation when using the DBAPI or SQLAlchemy.
Root cause
Connection.transaction_checkout() calls self._transaction.begin() on L413. This was likely written before inline begin support was added to the Python client library. Inline begin landed in PR #740 (Dec 2022), but transaction_checkout was not updated to take advantage of it.
The fix is to remove the self._transaction.begin() call, letting execute_sql() / execute_update() / batch_update() use their existing inline begin logic via _make_txn_selector().
This is also fully conformant with PEP 249 (DB-API 2.0), which does not define a begin() method — transactions are implicit, and the mechanism by which the driver starts the server-side transaction is an implementation detail.
Proposed fix
Draft PR: #1502