Skip to content

fix(postgres,pgx): roll back aborted explicit transaction on migration error - #1446

Open
cipherprofessor wants to merge 1 commit into
golang-migrate:masterfrom
cipherprofessor:fix/rollback-explicit-transaction-on-error
Open

cipherprofessor wants to merge 1 commit into
golang-migrate:masterfrom
cipherprofessor:fix/rollback-explicit-transaction-on-error

Conversation

@cipherprofessor

Copy link
Copy Markdown

Fixes #581.

Problem

A migration script using an explicit BEGIN/COMMIT block (which this project's own TUTORIAL.md recommends) is not automatically rolled back by Postgres when a statement inside it errors — unlike an implicit transaction, which Postgres's simple query protocol rolls back automatically on error. Left un-rolled-back, the connection stays in an aborted-transaction state, and every subsequent statement on it fails, including the advisory-unlock call migrate issues after a failed run — surfacing a confusing second error on top of the real one, exactly as described in #581.

Fix

Identical across all three Postgres drivers (postgres, pgx, pgx/v5): runStatement now issues a ROLLBACK after a failed ExecContext, before building the returned error.

  • Verified empirically (not assumed) that ROLLBACK with no explicit transaction open is a safe no-op — Postgres reports only a WARNING, not an error, and the connection remains fully usable afterward. Checked directly through the actual driver code path, not just psql.
  • If the statement itself failed with a rich diagnostic (pgErr — message, line/column, detail), that's preserved even if the recovery ROLLBACK also fails: the rollback failure is appended as additional context and joined via errors.Join (matching this package's own existing convention in SetVersion), rather than discarding the original diagnostic.
  • The recovery ROLLBACK gets its own fresh timeout window sized to the driver's configured StatementTimeout, rather than being unbounded or reusing the original (possibly already-expired) context.

Testing

This project's own dktest-based Docker test suite couldn't run in my environment — the pinned dhui/dktest@v0.4.6 dependency hardcodes Docker API version 1.41 (client.WithVersion("1.41")) even with DOCKER_API_VERSION set, which my Docker daemon (minimum supported API 1.44) rejects. I confirmed this is a pre-existing, diff-independent environment limitation, not something this change introduces: an unrelated, untouched existing test (testMultipleStatements) fails identically against unmodified master.

To still verify with real rigor, I ran a real Postgres 16 container directly and exercised the actual driver code (Open/Lock/Run/Unlock) against it via a standalone program — both before the fix (red: Unlock fails with current transaction is aborted) and after (green: Unlock succeeds) — for both the postgres and pgx/v5 drivers. I also confirmed the fix doesn't change the exact error message testErrorParsing asserts on for a plain (non-transactional) syntax error, by re-running that exact scenario against real Postgres before and after.

I've added this project's own standard dktest-based regression test (testExplicitTransactionRolledBackOnError / TestExplicitTransactionRolledBackOnError) to all three driver test files, matching each file's existing convention, so CI (which has a working Docker setup) exercises this directly even though I couldn't run it locally.

go build, go vet, and gofmt are all clean.

Related

There's an existing, never-upstreamed fix for this same issue sitting in a fork (referenced in a comment on #581) — I read it for context but implemented this independently with my own tests, not copied from it.

…n error

A migration script using an explicit BEGIN/COMMIT block (which this
project's own TUTORIAL.md recommends) is not automatically rolled back
by Postgres when a statement inside it errors -- unlike an implicit
transaction, which Postgres's simple query protocol rolls back
automatically on error. Left un-rolled-back, the connection stays in
an aborted-transaction state, and every subsequent statement on it
fails, including the advisory-unlock call migrate issues after a
failed run -- surfacing a confusing second error on top of the real
one, exactly as described in golang-migrate#581.

Fixed identically across all three Postgres drivers (postgres, pgx,
pgx/v5): runStatement now issues a ROLLBACK after a failed
ExecContext, before building the returned error. Verified empirically
(not assumed) that ROLLBACK with no explicit transaction open is a
safe no-op: Postgres reports only a WARNING, not an error, and the
connection remains fully usable afterward -- checked directly via the
same driver code path used here, not just psql.

Review before committing caught two real issues with the first draft,
both fixed:
- The original draft discarded the statement's own rich pgErr
  diagnostic (message, line/column, detail) entirely if the recovery
  ROLLBACK itself failed, replacing it with a bare rollback-failure
  error. Now both are preserved: the original error message/line/
  column is still built from the statement's own error, a rollback
  failure is appended as additional context rather than replacing it,
  and OrigErr joins both errors (errors.Join), matching this package's
  own existing convention for combining a statement error with a
  rollback error (see SetVersion).
- The recovery ROLLBACK was issued on an unbounded context.Background(),
  ignoring the driver's own configured StatementTimeout. It now gets
  its own fresh timeout window sized to that same config value, rather
  than being unbounded or reusing the original ctx (which may already
  be expired if a timeout is what caused the statement to fail).

Testing: this project's own dktest-based Docker test suite can't run
in this environment -- the pinned dhui/dktest@v0.4.6 dependency
hardcodes Docker API version 1.41 (client.WithVersion("1.41")) even
with DOCKER_API_VERSION set, which this Docker daemon (minimum
supported API 1.44) rejects. Confirmed this is a pre-existing,
diff-independent environment limitation, not something introduced
here: an unrelated, untouched existing test (testMultipleStatements)
fails identically on unmodified code. Verified the fix instead by
running a real Postgres 16 container directly and exercising the
actual unmodified driver code (Open/Lock/Run/Unlock) against it via a
standalone program, both before the fix (red: Unlock fails with
"current transaction is aborted") and after (green: Unlock succeeds)
-- for both the postgres and pgx/v5 drivers. Also confirmed the fix
doesn't change the exact error message an existing test
(testErrorParsing) asserts on for a plain (non-transactional) syntax
error, by re-running that exact scenario against real Postgres before
and after.

Added the project's own standard dktest-based regression test
(testExplicitTransactionRolledBackOnError / TestExplicitTransactionRolledBackOnError)
to all three driver test files, matching each file's existing
convention, so CI (which has a working Docker setup) exercises this
directly going forward even though it couldn't be run locally here.

go build, go vet, and gofmt are all clean.

Fixes golang-migrate#581

This branch has not been deployed

No deployments
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.

Explicit transactions not rolled back in case of error (postgres/pgx drivers)

1 participant