Skip to content

feat(dbapi): use inline begin to eliminate BeginTransaction RPC#1502

Open
waiho-gumloop wants to merge 1 commit intogoogleapis:mainfrom
waiho-gumloop:feat/dbapi-inline-begin
Open

feat(dbapi): use inline begin to eliminate BeginTransaction RPC#1502
waiho-gumloop wants to merge 1 commit intogoogleapis:mainfrom
waiho-gumloop:feat/dbapi-inline-begin

Conversation

@waiho-gumloop
Copy link

@waiho-gumloop waiho-gumloop commented Feb 26, 2026

Fixes #1503

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.pytransaction_checkout():

  • Removed self._transaction.begin() on 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

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).

Specific safety analysis:

  1. transaction_checkout() callers always execute SQL immediately: It's only called from run_statement()execute_sql() and batch_dml_executorbatch_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).

  3. rollback() handles _transaction_id is None: Skips the RPC — correct when no server-side transaction exists (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).

  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). The spec does not define a begin() method — transactions are implicit. The PEP author clarified on the DB-SIG mailing list 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, Dec 2022). The run_in_transaction path was updated to use inline begin, but transaction_checkout was not.

Test plan

  • All 198 DBAPI unit tests pass (tests/unit/spanner_dbapi/)
  • All 53 DBAPI system tests pass against Spanner emulator (tests/system/test_dbapi.py)
  • New unit test verifies begin() is not called by transaction_checkout()
  • CI system tests (will run automatically)

@product-auto-label product-auto-label bot added size: s Pull request size is small. api: spanner Issues related to the googleapis/python-spanner API. labels Feb 26, 2026
@google-cla
Copy link

google-cla bot commented Feb 26, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @waiho-gumloop, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request optimizes transaction handling in the Spanner DBAPI by leveraging the existing inline begin mechanism. It removes an unnecessary explicit BeginTransaction gRPC call, allowing transactions to be initiated implicitly with the first ExecuteSql or ExecuteBatchDml request. This change streamlines the transaction workflow, reduces network overhead, and improves performance by saving one RPC round-trip per transaction.

Highlights

  • Eliminated BeginTransaction RPC: Removed the explicit _transaction.begin() call in Connection.transaction_checkout(), allowing the first SQL execution to initiate the transaction via inline begin.
  • Performance Improvement: Reduced one gRPC round-trip per transaction, resulting in approximately 16ms savings on the Spanner emulator.
  • Docstring Update: Updated the transaction_checkout() docstring to explain the new inline begin behavior.
  • New Unit Test: Added test_transaction_checkout_does_not_call_begin to verify that begin() is no longer explicitly called.
Changelog
  • google/cloud/spanner_dbapi/connection.py
    • Removed the explicit call to self._transaction.begin() within the transaction_checkout method.
    • Updated the docstring for transaction_checkout to describe the inline begin behavior and the elimination of a separate BeginTransaction RPC.
  • tests/unit/spanner_dbapi/test_connection.py
    • Added a new unit test test_transaction_checkout_does_not_call_begin to assert that Transaction.begin() is not called by transaction_checkout.
Activity
  • All 198 existing DBAPI unit tests have passed.
  • All 53 existing DBAPI system tests have passed against the Spanner emulator.
  • A new unit test was added to specifically verify the absence of the begin() call in `transaction_checkout().
  • CI system tests are configured to run automatically.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

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 valuable optimization by removing the explicit BeginTransaction RPC call in transaction_checkout. Instead, it leverages the existing inline begin functionality, where the transaction is initiated with the first SQL execution. This change effectively reduces latency by saving one network round-trip per transaction. The modification is well-supported by a new unit test ensuring begin() is not called, and the detailed description provides a thorough safety analysis, confirming the change is sound.

@waiho-gumloop waiho-gumloop force-pushed the feat/dbapi-inline-begin branch from f176484 to 880081e Compare February 26, 2026 07:21
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: s Pull request size is small.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DBAPI: transaction_checkout() uses explicit BeginTransaction RPC instead of inline begin

2 participants