Skip to content

Add Conn.TxFromCurrentTransaction and TxOptions.BeginSQL - #2637

Open
timofurrer wants to merge 1 commit into
jackc:masterfrom
timofurrer:tx-from-current-transaction
Open

Add Conn.TxFromCurrentTransaction and TxOptions.BeginSQL#2637
timofurrer wants to merge 1 commit into
jackc:masterfrom
timofurrer:tx-from-current-transaction

Conversation

@timofurrer

Copy link
Copy Markdown

Implements the primitive suggested in #1667:

Since you want a Tx, maybe a function that constructs one without actually sending the begin query.

Design discussion and measurements are in #1667 (comment). Opening as a draft because naming and shape are still open.

What this adds

// BeginSQL returns the SQL statement that Conn.BeginTx would send for txOptions.
func (txOptions TxOptions) BeginSQL() string

// TxFromCurrentTransaction returns a Tx for the transaction c is already in,
// without sending a begin query.
func (c *Conn) TxFromCurrentTransaction(txOptions TxOptions) (Tx, error)

A caller can already queue begin as the first query of a Batch or a pgconn pipeline, which saves the round trip Conn.BeginTx spends on its own Exec. What is missing is a way to get a Tx back afterwards, so the rest of the unit of work keeps pgx's transaction bookkeeping (ErrTxClosed, Rollback being safe after Commit, ErrTxCommitRollback, savepoint nesting) instead of reimplementing it outside pgx.

b := &pgx.Batch{}
b.Queue(txOptions.BeginSQL())
b.Queue(fenceCheck, shardID, fenceID).QueryRow(func(row pgx.Row) error { return row.Scan(new(int)) })
batchErr := conn.SendBatch(ctx, b).Close()

tx, err := conn.TxFromCurrentTransaction(txOptions)

BeginSQL is exported so callers queueing the begin themselves use the statement pgx would produce, rather than hand-rolling one that silently drifts from beginSQL().

Notes on the implementation

  • Only TxOptions.CommitQuery is read. The isolation level, access mode and deferrable mode must already have been established by the begin query the caller sent, and this is documented on the method.
  • The guard accepts transaction status 'T' and 'E'. A batch that failed partway leaves the connection in a failed transaction, and the caller still needs a Tx to roll it back.
  • The returned value is the same unexported dbTx that Conn.BeginTx returns, so behavior after construction is identical. No existing code path changes.

Testing

New tests in tx_test.go: table-driven coverage of BeginSQL, plus commit, rollback, not-in-a-transaction, failed-transaction and CommitQuery cases for TxFromCurrentTransaction. The commit test also asserts the isolation level from the batched begin is in effect inside the resulting Tx.

Full go test for the root package passes against PostgreSQL 17.

Not addressed

You raised on #1667 that COMMIT is the symmetric problem. It is, and this does not solve it: a caller can queue commit in the final batch, but nothing can tell the Tx it is already closed, so defer tx.Rollback() spends the round trip that was just saved. That seems to want its own affordance and I did not want to conflate the two.

AI disclosure

Per CONTRIBUTING.md: this change was developed with AI assistance (Claude Code, Opus 5), including the prototype, the round-trip measurements and the tests. I have reviewed it and can answer questions about it.

Allows a caller to begin a transaction as part of a Batch or a pgconn
pipeline and still get a Tx for the rest of the unit of work, saving the
round trip that Conn.BeginTx spends on its own begin query.

TxOptions.BeginSQL exports the statement Conn.BeginTx would send, so
callers queueing it themselves do not have to hand-roll it.

Refs jackc#1667
@timofurrer
timofurrer marked this pull request as ready for review August 22, 2026 10:30
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