Skip to content

Expose the transaction status the server reports (Postgres, MySQL, SQLite) - #4512

Open
alandariva wants to merge 1 commit into
transact-rs:mainfrom
alandariva:transaction-status
Open

alandariva wants to merge 1 commit into
transact-rs:mainfrom
alandariva:transaction-status

Conversation

@alandariva

Copy link
Copy Markdown

Each database already tells the client whether a session is in a transaction, and SQLx reads it, but keeps it private (pub(crate) fn in_transaction). The public Connection::is_in_transaction() counts the transactions begun through SQLx (transaction_depth), so it cannot see a BEGIN sent as a query, a transaction begun inside a MySQL stored procedure, a rollback after a deadlock, or one SQLite ended on its own. Tools that run user-written SQL (SQL editors, REPLs) need that state to show it and to avoid committing or rolling back work silently; today they have to probe with extra queries, which can change what the session sees (a PostgreSQL SELECT takes the transaction's snapshot, so a later SET TRANSACTION ISOLATION LEVEL fails).

This adds read-only accessors and changes nothing else:

  • Postgres: PgConnection::transaction_status() returns the status of the last ReadyForQuery (Idle, Transaction, Error). It first reads responses still pending (wait_until_ready), so it is current after a failed statement or a dropped stream; no query is sent. The existing private enum becomes public as PgTransactionStatus (renamed from TransactionStatus, with Clone, Copy, PartialEq, Eq).
  • MySQL: MySqlConnection::server_in_transaction() and server_autocommit() read SERVER_STATUS_IN_TRANS / SERVER_STATUS_AUTOCOMMIT from the last OK or EOF packet. Two small fixes make them reliable:
    • ping() now stores the status of its OK packet. ERR packets carry no status, so after a failed statement (for example a deadlock victim, whose transaction the server rolled back) a ping reads it again without running a statement.
    • A new connection takes the status from the handshake (and the final auth OK when it is parsed) instead of starting empty, so server_autocommit() is right before the first query even when set_names, timezone and the sql_mode options are all off.
  • SQLite: LockedSqliteHandle::is_autocommit() wraps sqlite3_get_autocommit, so callers do not need unsafe and the semver-exempt libsqlite3-sys type.

Tests (tests/postgres, tests/mysql, tests/sqlite) cover BEGIN sent as a query, a partly read result, a failed statement, COMMIT AND CHAIN, a transaction begun inside a procedure, autocommit, the status before the first query, a deadlock between two connections and INSERT OR ROLLBACK. Each was checked to fail without its fix (PostgreSQL 18, MySQL 8.4, bundled SQLite); the SQLite suite passes with --test-threads=1.

Related: #4469 asks the pool to use the same ReadyForQuery state internally to skip the release ping; this PR only exposes it to callers.

We have been running this on top of 0.9.0 in a desktop SQL client to show the session's real transaction state.

Does your PR solve an issue?

No existing issue. It relates to #4469 (same protocol state, used internally by the pool).

Is this a breaking change?

No. It only adds public methods and a public type. The type was private before, so the rename breaks nothing. The MySQL status flags now start from the handshake and are refreshed by ping(). The only internal reader of those flags is MySqlTransactionManager::begin, which checks them right after its own BEGIN, so its behaviour does not change.

- `PgConnection::transaction_status()` returns the status of the last
  `ReadyForQuery` message, after reading responses still pending; the
  status type is now public as `PgTransactionStatus`.
- `MySqlConnection::server_in_transaction()` and `server_autocommit()`
  read the status flags of the last OK or EOF packet. `ping()` now
  refreshes them (ERR packets carry none), and a new connection takes
  them from the handshake instead of starting empty.
- `LockedSqliteHandle::is_autocommit()` wraps `sqlite3_get_autocommit`.

Unlike `Connection::is_in_transaction()`, which counts the transactions
begun through SQLx, these reflect transactions begun or ended by
statements sent as queries, inside stored procedures, or by the server
itself (a deadlock, `INSERT OR ROLLBACK`, an interrupted write).
alandariva added a commit to alandariva/jykre-studio that referenced this pull request Nov 8, 2026
- ADR-0030 (transaction state through a pinned SQLx fork) is accepted and
  implemented; the references no longer call it proposed.
- ADR-0027 notes that SQLite now reports its transaction state through the
  fork, so only whether a transaction wrote is inferred.
- TD-001 and ADR-0030 link transact-rs/sqlx#4512, which proposes the same
  readers upstream, rebased on its main.

cargo xtask check passed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant