Repository navigation
fix!: run query() in a READ ONLY transaction, as documented - #64
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
BREAKING CHANGE: uow.query() now opens its transaction READ ONLY. A write inside it (INSERT, UPDATE, DELETE, DDL, nextval()) fails with SQLSTATE 25006 instead of being rolled back silently, or persisted when followed by an explicit session.commit(). Move writes to uow.transaction().
AlexeyShalaev
force-pushed
the
fix/56-read-only-query
branch
from
October 16, 2026 19:08
e9b700a to
a4a3feb
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #56.
Problem
query()was documented as "read-only ... does not start a transaction", and rule 9 called an advisory lock taken in it "a no-op". Measured on PostgreSQL 17, the block is an ordinary autobegun transaction from its first statement until it exits. It holds its locks, sitsidle in transactionbetween statements, and a write followed byqx.session.commit()is persisted.Change
query()makes its transactionREAD ONLY. When the session hasn't checked out a connection yet, that happens throughsession.connection(execution_options={"postgresql_readonly": True}), so asyncpg opens the transactionREAD ONLYwith no extra round trip, and the dialect resets the flag when the connection returns to the pool (covered by a test on a one-connection pool). Whenopen_session()has already started the transaction (an isolation level was applied, or an override ran a statement, like theTenantUnitOfWorkexample in its docstring), the block issuesSET TRANSACTION READ ONLY, which PostgreSQL accepts at that point.uow/sqlalchemy.py,uow/protocols.py), the advanced guide anddocs/agents.mddescribe the block as what it is: one read-only transaction, rolled back on exit, that holds its locks and itsnow()until then. Rule 5 is rewritten. Rule 9 now says the lock is held for the whole block. The method table and the WRONG/RIGHT example are corrected, and there's a new "Fixed since" row.Checked before choosing:
pg_try_advisory_xact_lock,set_config(..., true)(thesearch_pathlistener) andSET LOCALall work in a READ ONLY transaction.nextval()doesn't.Considered and rejected
after_beginlistener that marks every transaction in the block, including one started after an explicitcommit(). It costs a round trip per block on the common path, to cover a misuse (committing insidequery()) that the docstring now names.Breaking change
fix!:. Code that writes throughquery(), includingnextval()or a volatile function that writes, now fails with SQLSTATE 25006 (ReadOnlySQLTransactionError) instead of being silently rolled back, or persisted after an explicit commit. Migration: move the write totransaction(). The commit carries aBREAKING CHANGE:footer, so release-please will list it, and it bumps the minor (0.5.0) underbump-minor-pre-major. No org repo pinssqlalchemy-foundation-kit(I grepped every siblingpyproject.toml), so no cap trips.Evidence
New integration tests against the old
uow/sqlalchemy.py(swapped in fromorigin/master): 4 failed, 4 passed. The failures are the reporter's INSERT + commit,transaction_read_onlyinside the block, the isolation-level path, and theopen_sessionoverride path. The 4 that pass on both are regression guards for behaviour the new docs describe: the advisory lock is held until exit, the pooled connection is read-write again afterwards, and two read paths.The reporter's script after the fix: lines 1–2 unchanged (they describe a transaction, as the docs now say), and step 3 raises:
Also fixed: a telemetry unit test whose
AsyncMocksession made the syncin_transaction()return a coroutine.Gate:
make checkclean;make test632 passed.