Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 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.

## 3.5.11

- Adding `JsonbListView` with `isSqlNull(int index)` method to check if a JSONB array has SQL or JSON null value.
Expand Down
47 changes: 39 additions & 8 deletions lib/src/v3/connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -588,14 +595,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
Expand All @@ -609,10 +624,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;
Expand Down Expand Up @@ -1227,8 +1245,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
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
46 changes: 46 additions & 0 deletions test/transaction_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -836,5 +836,51 @@ 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<PgException>()),
);

final rows = await conn.execute('SELECT 1');
expect(rows, [
[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');
},
);
});
}
Loading