Repository: isoos/postgresql-dart

Issue:
Migration for PostgreSQLExecutionContext.cancelTransaction()?
In the old version, I used this:

```
await connection.runTx((ctx) async {
   //...Do Stuff
   if(condition){
     ctx.cancelTransaction();
   }
}
```

With the new version, I found no cancelTransaction method for Session. So how to migrate it (Or: how to cancel a transaction based on a condition)?


Discussion: Good point, this may seems to be an oversight. The quick-to-unblock way is to throw an exception which you catch outside, but I shall prepare a PR for this soon. 

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45bc35e9..18d745de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,8 @@
 ## 3.0.1
 
 - Fix: do not allow `execute` after closing the `Connection`.
+- `Session.runTx()` supports rolling back the transaction through `TxSession.rollback()`
+  (otherwise any exception has the same effect, but callers need to catch it).
 
 ## 3.0.0
 
diff --git a/lib/postgres.dart b/lib/postgres.dart
index 672f0853..9977488a 100644
--- a/lib/postgres.dart
+++ b/lib/postgres.dart
@@ -169,6 +169,13 @@ abstract class Session {
   });
 }
 
+/// A [Session] with transaction-related helper method(s).
+abstract class TxSession extends Session {
+  /// Executes `ROLLBACK` and closes the transaction session.
+  /// Further queries will throw an exception.
+  Future<void> rollback();
+}
+
 abstract class SessionExecutor {
   /// Obtains a [Session] capable of running statements and calls [fn] with
   /// it.
@@ -188,7 +195,7 @@ abstract class SessionExecutor {
   /// Note that other invocations on a [Connection] are blocked while a
   /// transaction is active.
   Future<R> runTx<R>(
-    Future<R> Function(Session session) fn, {
+    Future<R> Function(TxSession session) fn, {
     TransactionSettings? settings,
   });
 
diff --git a/lib/src/pool/pool_api.dart b/lib/src/pool/pool_api.dart
index c56e65ea..92dc4e3b 100644
--- a/lib/src/pool/pool_api.dart
+++ b/lib/src/pool/pool_api.dart
@@ -87,7 +87,7 @@ abstract class Pool<L> implements Session, SessionExecutor {
 
   @override
   Future<R> runTx<R>(
-    Future<R> Function(Session session) fn, {
+    Future<R> Function(TxSession session) fn, {
     TransactionSettings? settings,
     L? locality,
   });
diff --git a/lib/src/pool/pool_impl.dart b/lib/src/pool/pool_impl.dart
index 39e569a8..3cc327b7 100644
--- a/lib/src/pool/pool_impl.dart
+++ b/lib/src/pool/pool_impl.dart
@@ -112,7 +112,7 @@ class PoolImplementation<L> implements Pool<L> {
 
   @override
   Future<R> runTx<R>(
-    Future<R> Function(Session session) fn, {
+    Future<R> Function(TxSession session) fn, {
     TransactionSettings? settings,
     L? locality,
   }) {
@@ -309,7 +309,7 @@ class _PoolConnection implements Connection {
 
   @override
   Future<R> runTx<R>(
-    Future<R> Function(Session session) fn, {
+    Future<R> Function(TxSession session) fn, {
     TransactionSettings? settings,
   }) {
     return _connection.runTx(
diff --git a/lib/src/v3/connection.dart b/lib/src/v3/connection.dart
index 51bf7eb8..93be3efa 100644
--- a/lib/src/v3/connection.dart
+++ b/lib/src/v3/connection.dart
@@ -395,7 +395,7 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection {
 
   @override
   Future<R> runTx<R>(
-    Future<R> Function(Session session) fn, {
+    Future<R> Function(TxSession session) fn, {
     TransactionSettings? settings,
   }) {
     final rsettings = ResolvedTransactionSettings(settings, _settings);
@@ -428,7 +428,11 @@ class PgConnectionImplementation extends _PgSessionBase implements Connection {
 
       try {
         final result = await fn(transaction);
-        await transaction._sendAndMarkClosed('COMMIT;');
+        if (transaction.mayCommit) {
+          await transaction._sendAndMarkClosed('COMMIT;');
+        } else if (!transaction._sessionClosed) {
+          await transaction._sendAndMarkClosed('ROLLBACK;');
+        }
 
         // If we have received an error while the transaction was active, it
         // will always be rolled back.
@@ -866,12 +870,13 @@ class _RegularSession extends _PgSessionBase {
   _RegularSession(this._connection, this._settings);
 }
 
-class _TransactionSession extends _PgSessionBase {
+class _TransactionSession extends _PgSessionBase implements TxSession {
   @override
   final PgConnectionImplementation _connection;
   @override
   final ResolvedTransactionSettings _settings;
 
+  bool _closing = false;
   PgException? _transactionException;
 
   _TransactionSession(this._connection, this._settings);
@@ -882,6 +887,7 @@ class _TransactionSession extends _PgSessionBase {
   /// This prevents other pending operations on the transaction that haven't
   /// been awaited from running.
   Future<void> _sendAndMarkClosed(String command) async {
+    _closing = true;
     final controller = StreamController<ResultRow>();
     final items = <ResultRow>[];
 
@@ -899,6 +905,16 @@ class _TransactionSession extends _PgSessionBase {
     await querySubscription.asFuture();
     await querySubscription.cancel();
   }
+
+  @override
+  Future<void> rollback() async {
+    await _sendAndMarkClosed('ROLLBACK;');
+  }
+
+  bool get mayCommit =>
+      !_closing &&
+      _connection._activeTransaction == this &&
+      _transactionException == null;
 }
 
 abstract class _PendingOperation {


Test patch:
diff --git a/test/transaction_test.dart b/test/transaction_test.dart
index c22638fc..b852a608 100644
--- a/test/transaction_test.dart
+++ b/test/transaction_test.dart
@@ -579,7 +579,7 @@ void main() {
       await conn.close();
     });
 
-    test('Is rolled back/executes later query', () async {
+    test('Is rolled back/executes later query after exception', () async {
       expect(conn.runTx((c) async {
         await c.execute('INSERT INTO t (id) VALUES (1)');
         throw Exception();
@@ -589,6 +589,23 @@ void main() {
       expect(noRows, []);
     });
 
+    test('Is rolled back/executes later query after calling `rollback()`.',
+        () async {
+      final rs = await conn.runTx((c) async {
+        await c.execute('INSERT INTO t (id) VALUES (1)');
+        final stm = await c.prepare('SELECT 1');
+        expect(await stm.run([]), hasLength(1));
+        await c.rollback();
+        await expectLater(() => c.execute('SELECT 1'), throwsException);
+        await expectLater(() => stm.run([]), throwsException);
+        return 123;
+      });
+      expect(rs, 123);
+
+      final noRows = await conn.execute('SELECT id FROM t');
+      expect(noRows, []);
+    });
+
     test('Executes pending query', () async {
       final orderEnsurer = [];