Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

chore: test minimum dependencies in python 3.7#740

Merged
asthamohta merged 1 commit into
maingoogleapis/python-spanner:mainfrom
use-min-dependencies-in-3.7googleapis/python-spanner:use-min-dependencies-in-3.7Copy head branch name to clipboard
Jun 6, 2022
Merged

chore: test minimum dependencies in python 3.7#740
asthamohta merged 1 commit into
maingoogleapis/python-spanner:mainfrom
use-min-dependencies-in-3.7googleapis/python-spanner:use-min-dependencies-in-3.7Copy head branch name to clipboard

Conversation

@parthea

@parthea parthea commented Jun 3, 2022

Copy link
Copy Markdown
Contributor

Test the minimum supported dependencies in python 3.7 unit tests to prepare for dropping python 3.6

@parthea parthea requested review from a team June 3, 2022 11:57
@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 Jun 3, 2022
@asthamohta asthamohta merged commit b47791a into main Jun 6, 2022
@asthamohta asthamohta deleted the use-min-dependencies-in-3.7 branch June 6, 2022 06:17
olavloite added a commit that referenced this pull request Mar 13, 2026
Fixes 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, 2026
Fixes #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>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

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.

3 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.