From ebee6157a2d2dfc4e89f26744a9f39b455b3d8db Mon Sep 17 00:00:00 2001 From: kartikey321 Date: Sat, 6 Jun 2026 23:27:32 +0530 Subject: [PATCH 1/4] Fix connection left in broken state when BEGIN fails or ROLLBACK errors Two cleanup gaps in runTx: 1. _activeTransaction was assigned before BEGIN entered the try block. If BEGIN was rejected by PostgreSQL the assignment was never unwound, permanently blocking the connection with "inside a runTx call" on every subsequent execute() call. Fix: move BEGIN inside the try block so a failed BEGIN goes through the same cleanup path as any other error. 2. When ROLLBACK itself failed and the original exception was a PgException, the rollback failure was silently swallowed and the connection was returned to the pool in an undefined PostgreSQL transaction state. Fix: call _closeAfterError() on both ROLLBACK call sites when ROLLBACK fails, matching pgx which explicitly kills the connection in this case. --- lib/src/v3/connection.dart | 23 +++++++++++++++++------ test/transaction_test.dart | 21 +++++++++++++++++++++ 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/lib/src/v3/connection.dart b/lib/src/v3/connection.dart index f82ee58..3fd3da2 100644 --- a/lib/src/v3/connection.dart +++ b/lib/src/v3/connection.dart @@ -588,14 +588,22 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection { beginQuery = 'BEGIN;'; } - await transaction.execute(Sql(beginQuery), queryMode: QueryMode.simple); - try { + await transaction.execute(Sql(beginQuery), queryMode: QueryMode.simple); + final result = await fn(transaction); if (transaction.mayCommit) { await transaction._sendAndMarkClosed('COMMIT;'); } else if (!transaction._sessionClosed) { - await transaction._sendAndMarkClosed('ROLLBACK;'); + try { + await transaction._sendAndMarkClosed('ROLLBACK;'); + } catch (rollbackEx) { + // ROLLBACK failed — PG connection state is undefined, close it. + _connection._closeAfterError( + rollbackEx is PgException ? rollbackEx : null, + ); + rethrow; + } } // If we have received an error while the transaction was active, it @@ -609,10 +617,13 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection { if (!transaction._sessionClosed) { try { await transaction._sendAndMarkClosed('ROLLBACK;'); - } catch (_) { - // checking the outer exception + } catch (rollbackEx) { + // ROLLBACK failed — PG connection state is undefined, close it. + _connection._closeAfterError( + rollbackEx is PgException ? rollbackEx : null, + ); if (e is PgException) { - // Ignore exception of rollback, as the earlier exception takes precedence. + // Original exception takes precedence over rollback failure. } else { // Do not ignore the exception here, it may be an implementation bug we are swallowing. rethrow; diff --git a/test/transaction_test.dart b/test/transaction_test.dart index 2a5ce87..1d2e70a 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -836,5 +836,26 @@ void main() { } }, ); + + test('failed BEGIN during runTx clears active transaction state', () async { + await conn.execute('BEGIN'); + try { + await conn.execute('SELECT 1 FROM _nonexistent_xyz_ LIMIT 1'); + } catch (_) { + // Leave the manually-started transaction in PostgreSQL's failed state. + } + + await expectLater( + conn.runTx((tx) async { + await tx.execute('SELECT 1'); + }), + throwsA(isA()), + ); + + final rows = await conn.execute('SELECT 1'); + expect(rows, [ + [1], + ]); + }); }); } From 600d3347c323c6672f712a129817f5bea0d34ccd Mon Sep 17 00:00:00 2001 From: kartikey321 Date: Sun, 7 Jun 2026 08:13:46 +0530 Subject: [PATCH 2/4] Fix _sendAndMarkClosed cleanup race and update changelog --- CHANGELOG.md | 5 +++++ lib/src/v3/connection.dart | 17 +++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29d8516..ee55b03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 3.5.12 + +- Fix connection permanently blocked when `BEGIN` fails inside `runTx` (stale `_activeTransaction` state). +- Fix connection left in undefined PostgreSQL state when `ROLLBACK` fails after a transaction error. + ## 3.5.11 - Adding `JsonbListView` with `isSqlNull(int index)` method to check if a JSONB array has SQL or JSON null value. diff --git a/lib/src/v3/connection.dart b/lib/src/v3/connection.dart index 3fd3da2..12b1ac8 100644 --- a/lib/src/v3/connection.dart +++ b/lib/src/v3/connection.dart @@ -1238,8 +1238,21 @@ class _TransactionSession extends _PgSessionBase implements TxSession { _connection._activeTransaction = null; }, ); - await querySubscription.asFuture(); - await querySubscription.cancel(); + Object? error; + StackTrace? stackTrace; + try { + await querySubscription.asFuture(); + } catch (e, s) { + error = e; + stackTrace = s; + await querySubscription._done.future; + } finally { + await querySubscription.cancel(); + } + + if (error != null) { + Error.throwWithStackTrace(error, stackTrace!); + } } @override From 00c743ed258536aa9b8b02b0d3eb761e636b094e Mon Sep 17 00:00:00 2001 From: kartikey321 Date: Sat, 6 Jun 2026 22:47:47 +0530 Subject: [PATCH 3/4] Fix stale _transactionException after ROLLBACK TO SAVEPOINT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When any PgException occurred inside runTx, _transactionException was set and never cleared — even after a successful ROLLBACK TO SAVEPOINT restored PostgreSQL to a healthy state. mayCommit therefore returned false and the driver sent ROLLBACK instead of COMMIT, silently discarding all subsequent work that completed successfully at the PostgreSQL level. Fix: clear _transactionException in _handleMessage whenever PostgreSQL sends ReadyForQuery with state='T' (InTransaction), which is the authoritative signal that the connection is in a clean, committable state. --- .gitignore | 5 ++++- CHANGELOG.md | 1 + lib/src/v3/connection.dart | 7 +++++++ pubspec.yaml | 2 +- test/transaction_test.dart | 25 +++++++++++++++++++++++++ 5 files changed, 38 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 24c8201..f5b1b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,7 @@ doc/api/ *.iws # VS Code IDE -.vscode/ \ No newline at end of file +.vscode/ + +# Reference libraries for bug research +_reference/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index ee55b03..d5191e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 3.5.12 +- Fix `runTx` silently rolling back after `ROLLBACK TO SAVEPOINT` recovery: clear stale `_transactionException` when PostgreSQL confirms a healthy transaction state. - Fix connection permanently blocked when `BEGIN` fails inside `runTx` (stale `_activeTransaction` state). - Fix connection left in undefined PostgreSQL state when `ROLLBACK` fails after a transaction error. diff --git a/lib/src/v3/connection.dart b/lib/src/v3/connection.dart index 12b1ac8..f0f069c 100644 --- a/lib/src/v3/connection.dart +++ b/lib/src/v3/connection.dart @@ -533,6 +533,13 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection { _pending!.handleError(exception); } } else if (_pending != null) { + // If PostgreSQL reports the transaction is healthy (e.g. after a + // successful ROLLBACK TO SAVEPOINT), clear any stale exception so that + // mayCommit can return true and the outer runTx can COMMIT. + if (message is ReadyForQueryMessage && + message.state == ReadyForQueryMessageState.transaction) { + _activeTransaction?._transactionException = null; + } await _pending!.handleMessage(message); } } finally { diff --git a/pubspec.yaml b/pubspec.yaml index 46bbd87..1190fa8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: postgres description: PostgreSQL database driver. Supports binary protocol, connection pooling and statement reuse. -version: 3.5.11 +version: 3.5.12 homepage: https://github.com/isoos/postgresql-dart topics: - sql diff --git a/test/transaction_test.dart b/test/transaction_test.dart index 1d2e70a..38c76ce 100644 --- a/test/transaction_test.dart +++ b/test/transaction_test.dart @@ -857,5 +857,30 @@ void main() { [1], ]); }); + + test( + 'ROLLBACK TO SAVEPOINT clears _transactionException so subsequent work commits', + () async { + await conn.runTx((tx) async { + await tx.execute('SAVEPOINT sp1'); + + try { + // 42P01 – relation does not exist + await tx.execute('SELECT 1 FROM _nonexistent_xyz_ LIMIT 1'); + } catch (_) { + await tx.execute('ROLLBACK TO SAVEPOINT sp1'); + await tx.execute('RELEASE SAVEPOINT sp1'); + } + + // This insert must commit; before the fix it was silently rolled back. + await tx.execute('INSERT INTO t (id) VALUES (42)'); + }); + + final rows = await conn.execute('SELECT id FROM t WHERE id = 42'); + expect(rows, [ + [42], + ], reason: 'Insert after savepoint recovery should have been committed'); + }, + ); }); } From 63dabd44b4681c55b17776800d73c148257bfbc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Istv=C3=A1n=20So=C3=B3s?= Date: Sun, 7 Jun 2026 11:27:25 +0200 Subject: [PATCH 4/4] Apply suggestion from @isoos --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index f5b1b2d..24c8201 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,4 @@ doc/api/ *.iws # VS Code IDE -.vscode/ - -# Reference libraries for bug research -_reference/ \ No newline at end of file +.vscode/ \ No newline at end of file