From 8b1b213204b72d731a8dd3374749d3e6862e073e Mon Sep 17 00:00:00 2001
From: Dave Cramer <davecramer@gmail.com>
Date: Tue, 4 Aug 2020 12:15:38 -0400
Subject: [PATCH] Throw error and rollback on a failed transaction instead of
 silently rolling back

---
 src/backend/tcop/utility.c                    | 9 +++++++++
 src/backend/utils/error/elog.c                | 1 +
 src/include/utils/elog.h                      | 9 +++++----
 src/test/regress/expected/copy2.out           | 2 ++
 src/test/regress/expected/create_index.out    | 6 ++++++
 src/test/regress/expected/foreign_key.out     | 7 +++++++
 src/test/regress/expected/insert_conflict.out | 3 +++
 src/test/regress/expected/portals.out         | 1 +
 src/test/regress/expected/prepared_xacts.out  | 1 +
 src/test/regress/expected/privileges.out      | 2 ++
 src/test/regress/expected/subscription.out    | 2 ++
 src/test/regress/expected/transactions.out    | 9 +++++++++
 src/test/regress/output/constraints.source    | 2 ++
 13 files changed, 50 insertions(+), 4 deletions(-)

diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 9b0c376c8c..ad462f00a6 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -621,6 +621,11 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 							/* report unsuccessful commit in qc */
 							if (qc)
 								SetQueryCompletion(qc, CMDTAG_ROLLBACK, 0);
+							/* report USER_ERROR so that we don't completely exit the context */
+							ereport(USER_ERROR,
+									(errcode(ERRCODE_TRANSACTION_ROLLBACK),
+											errmsg("current transaction failed, "
+												"rolling back")));
 						}
 						break;
 
@@ -630,6 +635,10 @@ standard_ProcessUtility(PlannedStmt *pstmt,
 							/* report unsuccessful commit in qc */
 							if (qc)
 								SetQueryCompletion(qc, CMDTAG_ROLLBACK, 0);
+							ereport(ERROR,
+									(errcode(ERRCODE_TRANSACTION_ROLLBACK),
+											errmsg("current transaction failed, "
+												"rolling back")));
 						}
 						break;
 
diff --git a/src/backend/utils/error/elog.c b/src/backend/utils/error/elog.c
index d0b368530e..b02a2dd20a 100644
--- a/src/backend/utils/error/elog.c
+++ b/src/backend/utils/error/elog.c
@@ -3414,6 +3414,7 @@ error_severity(int elevel)
 		case WARNING:
 			prefix = gettext_noop("WARNING");
 			break;
+		case USER_ERROR:
 		case ERROR:
 			prefix = gettext_noop("ERROR");
 			break;
diff --git a/src/include/utils/elog.h b/src/include/utils/elog.h
index 1e09ee0541..d3acbeb420 100644
--- a/src/include/utils/elog.h
+++ b/src/include/utils/elog.h
@@ -40,17 +40,18 @@
 #define WARNING		19			/* Warnings.  NOTICE is for expected messages
 								 * like implicit sequence creation by SERIAL.
 								 * WARNING is for unexpected messages. */
-#define ERROR		20			/* user error - abort transaction; return to
+#define USER_ERROR	20
+#define ERROR		21			/* user error - abort transaction; return to
 								 * known state */
 /* Save ERROR value in PGERROR so it can be restored when Win32 includes
  * modify it.  We have to use a constant rather than ERROR because macros
  * are expanded only when referenced outside macros.
  */
 #ifdef WIN32
-#define PGERROR		20
+#define PGERROR		21
 #endif
-#define FATAL		21			/* fatal error - abort process */
-#define PANIC		22			/* take down the other backends with me */
+#define FATAL		22			/* fatal error - abort process */
+#define PANIC		23			/* take down the other backends with me */
 
  /* #define DEBUG DEBUG1 */	/* Backward compatibility with pre-7.3 */
 
diff --git a/src/test/regress/expected/copy2.out b/src/test/regress/expected/copy2.out
index e40287d25a..59a9246f0c 100644
--- a/src/test/regress/expected/copy2.out
+++ b/src/test/regress/expected/copy2.out
@@ -367,6 +367,7 @@ SAVEPOINT s1;
 COPY vistest FROM stdin CSV FREEZE;
 ERROR:  cannot perform COPY FREEZE because the table was not created or truncated in the current subtransaction
 COMMIT;
+ERROR:  current transaction failed, rolling back
 BEGIN;
 INSERT INTO vistest VALUES ('z');
 SAVEPOINT s1;
@@ -375,6 +376,7 @@ ROLLBACK TO SAVEPOINT s1;
 COPY vistest FROM stdin CSV FREEZE;
 ERROR:  cannot perform COPY FREEZE because the table was not created or truncated in the current subtransaction
 COMMIT;
+ERROR:  current transaction failed, rolling back
 CREATE FUNCTION truncate_in_subxact() RETURNS VOID AS
 $$
 BEGIN
diff --git a/src/test/regress/expected/create_index.out b/src/test/regress/expected/create_index.out
index e3e6634d7e..92db8aa7b7 100644
--- a/src/test/regress/expected/create_index.out
+++ b/src/test/regress/expected/create_index.out
@@ -1393,6 +1393,7 @@ BEGIN;
 CREATE INDEX CONCURRENTLY concur_index7 ON concur_heap(f1);
 ERROR:  CREATE INDEX CONCURRENTLY cannot run inside a transaction block
 COMMIT;
+ERROR:  current transaction failed, rolling back
 -- But you can do a regular index build in a transaction
 BEGIN;
 CREATE INDEX std_index on concur_heap(f2);
@@ -1453,6 +1454,7 @@ INSERT INTO concur_temp VALUES (1, 'foo'), (2, 'bar');
 CREATE INDEX CONCURRENTLY concur_temp_ind ON concur_temp(f1);
 ERROR:  CREATE INDEX CONCURRENTLY cannot run inside a transaction block
 COMMIT;
+ERROR:  current transaction failed, rolling back
 -- ON COMMIT DELETE ROWS
 CREATE TEMP TABLE concur_temp (f1 int, f2 text)
   ON COMMIT DELETE ROWS;
@@ -2327,6 +2329,7 @@ BEGIN;
 REINDEX TABLE CONCURRENTLY concur_reindex_tab;
 ERROR:  REINDEX CONCURRENTLY cannot run inside a transaction block
 COMMIT;
+ERROR:  current transaction failed, rolling back
 REINDEX TABLE CONCURRENTLY pg_class; -- no catalog relation
 ERROR:  cannot reindex system catalogs concurrently
 REINDEX INDEX CONCURRENTLY pg_class_oid_index; -- no catalog index
@@ -2490,6 +2493,7 @@ BEGIN;
 REINDEX INDEX CONCURRENTLY concur_temp_ind_1;
 ERROR:  REINDEX CONCURRENTLY cannot run inside a transaction block
 COMMIT;
+ERROR:  current transaction failed, rolling back
 -- ON COMMIT DELETE ROWS
 CREATE TEMP TABLE concur_temp_tab_2 (c1 int, c2 text)
   ON COMMIT DELETE ROWS;
@@ -2506,6 +2510,7 @@ CREATE INDEX concur_temp_ind_3 ON concur_temp_tab_3(c2);
 REINDEX INDEX CONCURRENTLY concur_temp_ind_3;
 ERROR:  REINDEX CONCURRENTLY cannot run inside a transaction block
 COMMIT;
+ERROR:  current transaction failed, rolling back
 -- REINDEX SCHEMA processes all temporary relations
 CREATE TABLE reindex_temp_before AS
 SELECT oid, relname, relfilenode, relkind, reltoastrelid
@@ -2585,6 +2590,7 @@ BEGIN;
 REINDEX SCHEMA schema_to_reindex; -- failure, cannot run in a transaction
 ERROR:  REINDEX SCHEMA cannot run inside a transaction block
 END;
+ERROR:  current transaction failed, rolling back
 -- concurrently
 REINDEX SCHEMA CONCURRENTLY schema_to_reindex;
 -- Failure for unauthorized user
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 07bd5b6434..e6ef96670c 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1052,6 +1052,7 @@ INSERT INTO fktable VALUES (500, 1000);
 ERROR:  insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
 DETAIL:  Key (fk)=(1000) is not present in table "pktable".
 COMMIT;
+ERROR:  current transaction failed, rolling back
 DROP TABLE fktable, pktable;
 -- tricky behavior: according to SQL99, if a deferred constraint is set
 -- to 'immediate' mode, it should be checked for validity *immediately*,
@@ -1076,6 +1077,7 @@ DETAIL:  Key (fk)=(2000) is not present in table "pktable".
 INSERT INTO pktable VALUES (2000, 3); -- too late
 ERROR:  current transaction is aborted, commands ignored until end of transaction block
 COMMIT;
+ERROR:  current transaction failed, rolling back
 DROP TABLE fktable, pktable;
 -- deferrable, initially deferred
 CREATE TABLE pktable (
@@ -1231,12 +1233,14 @@ UPDATE pktable SET id = 10 WHERE id = 5;
 ERROR:  update or delete on table "pktable" violates foreign key constraint "fktable_fk_fkey" on table "fktable"
 DETAIL:  Key (id)=(5) is still referenced from table "fktable".
 COMMIT;
+ERROR:  current transaction failed, rolling back
 BEGIN;
 -- doesn't match PK, should throw error now
 INSERT INTO fktable VALUES (0, 20);
 ERROR:  insert or update on table "fktable" violates foreign key constraint "fktable_fk_fkey"
 DETAIL:  Key (fk)=(20) is not present in table "pktable".
 COMMIT;
+ERROR:  current transaction failed, rolling back
 -- try additional syntax
 ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
 -- illegal option
@@ -1489,11 +1493,13 @@ insert into fktable2 values(2);
 alter table fktable2 drop constraint fktable2_f1_fkey;
 ERROR:  cannot ALTER TABLE "fktable2" because it has pending trigger events
 commit;
+ERROR:  current transaction failed, rolling back
 begin;
 delete from pktable2 where f1 = 1;
 alter table fktable2 drop constraint fktable2_f1_fkey;
 ERROR:  cannot ALTER TABLE "pktable2" because it has pending trigger events
 commit;
+ERROR:  current transaction failed, rolling back
 drop table pktable2, fktable2;
 --
 -- Test keys that "look" different but compare as equal
@@ -2440,6 +2446,7 @@ INSERT INTO fkpart8.tbl2 VALUES(1);
 ALTER TABLE fkpart8.tbl2 DROP CONSTRAINT tbl2_f1_fkey;
 ERROR:  cannot ALTER TABLE "tbl2_p1" because it has pending trigger events
 COMMIT;
+ERROR:  current transaction failed, rolling back
 DROP SCHEMA fkpart8 CASCADE;
 NOTICE:  drop cascades to 2 other objects
 DETAIL:  drop cascades to table fkpart8.tbl1
diff --git a/src/test/regress/expected/insert_conflict.out b/src/test/regress/expected/insert_conflict.out
index 1338b2b23e..1ce531270d 100644
--- a/src/test/regress/expected/insert_conflict.out
+++ b/src/test/regress/expected/insert_conflict.out
@@ -704,16 +704,19 @@ insert into selfconflict values (4,1), (4,2) on conflict(f1) do update set f2 =
 ERROR:  ON CONFLICT DO UPDATE command cannot affect row a second time
 HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
 commit;
+ERROR:  current transaction failed, rolling back
 begin transaction isolation level repeatable read;
 insert into selfconflict values (5,1), (5,2) on conflict(f1) do update set f2 = 0;
 ERROR:  ON CONFLICT DO UPDATE command cannot affect row a second time
 HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
 commit;
+ERROR:  current transaction failed, rolling back
 begin transaction isolation level serializable;
 insert into selfconflict values (6,1), (6,2) on conflict(f1) do update set f2 = 0;
 ERROR:  ON CONFLICT DO UPDATE command cannot affect row a second time
 HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.
 commit;
+ERROR:  current transaction failed, rolling back
 select * from selfconflict;
  f1 | f2 
 ----+----
diff --git a/src/test/regress/expected/portals.out b/src/test/regress/expected/portals.out
index dc0d2ef7dd..6092d6acf7 100644
--- a/src/test/regress/expected/portals.out
+++ b/src/test/regress/expected/portals.out
@@ -715,6 +715,7 @@ FETCH BACKWARD 1 FROM foo24; -- should fail
 ERROR:  cursor can only scan forward
 HINT:  Declare it with SCROLL option to enable backward scan.
 END;
+ERROR:  current transaction failed, rolling back
 --
 -- Cursors outside transaction blocks
 --
diff --git a/src/test/regress/expected/prepared_xacts.out b/src/test/regress/expected/prepared_xacts.out
index eb77c18788..41e6bfd9b3 100644
--- a/src/test/regress/expected/prepared_xacts.out
+++ b/src/test/regress/expected/prepared_xacts.out
@@ -137,6 +137,7 @@ ERROR:  could not serialize access due to read/write dependencies among transact
 DETAIL:  Reason code: Canceled on identification as a pivot, during write.
 HINT:  The transaction might succeed if retried.
 PREPARE TRANSACTION 'foo5';
+ERROR:  current transaction failed, rolling back
 SELECT gid FROM pg_prepared_xacts;
  gid  
 ------
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 3ec22c20ea..fd4ed9d95b 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -114,6 +114,7 @@ BEGIN;
 LOCK atest2 IN ACCESS EXCLUSIVE MODE; -- fail
 ERROR:  permission denied for table atest2
 COMMIT;
+ERROR:  current transaction failed, rolling back
 COPY atest2 FROM stdin; -- fail
 ERROR:  permission denied for table atest2
 GRANT ALL ON atest1 TO PUBLIC; -- fail
@@ -727,6 +728,7 @@ BEGIN;
 LOCK atestc;
 ERROR:  permission denied for table atestc
 END;
+ERROR:  current transaction failed, rolling back
 -- privileges on functions, languages
 -- switch to superuser
 \c -
diff --git a/src/test/regress/expected/subscription.out b/src/test/regress/expected/subscription.out
index d71db0d520..4b215f0c8c 100644
--- a/src/test/regress/expected/subscription.out
+++ b/src/test/regress/expected/subscription.out
@@ -20,6 +20,7 @@ BEGIN;
 CREATE SUBSCRIPTION regress_testsub CONNECTION 'testconn' PUBLICATION testpub WITH (create_slot);
 ERROR:  CREATE SUBSCRIPTION ... WITH (create_slot = true) cannot run inside a transaction block
 COMMIT;
+ERROR:  current transaction failed, rolling back
 -- fail - invalid connection string
 CREATE SUBSCRIPTION regress_testsub CONNECTION 'testconn' PUBLICATION testpub;
 ERROR:  invalid connection string syntax: missing "=" after "testconn" in connection info string
@@ -146,6 +147,7 @@ BEGIN;
 DROP SUBSCRIPTION regress_testsub;
 ERROR:  DROP SUBSCRIPTION cannot run inside a transaction block
 COMMIT;
+ERROR:  current transaction failed, rolling back
 ALTER SUBSCRIPTION regress_testsub SET (slot_name = NONE);
 -- now it works
 BEGIN;
diff --git a/src/test/regress/expected/transactions.out b/src/test/regress/expected/transactions.out
index 1b03310029..89ca57709b 100644
--- a/src/test/regress/expected/transactions.out
+++ b/src/test/regress/expected/transactions.out
@@ -53,6 +53,7 @@ SELECT * FROM writetest; -- ok
 SET TRANSACTION READ WRITE; --fail
 ERROR:  transaction read-write mode must be set before any query
 COMMIT;
+ERROR:  current transaction failed, rolling back
 BEGIN;
 SET TRANSACTION READ ONLY; -- ok
 SET TRANSACTION READ WRITE; -- ok
@@ -73,6 +74,7 @@ SET TRANSACTION READ ONLY; -- ok
 SET TRANSACTION READ WRITE; --fail
 ERROR:  cannot set transaction read-write mode inside a read-only transaction
 COMMIT;
+ERROR:  current transaction failed, rolling back
 BEGIN;
 SET TRANSACTION READ WRITE; -- ok
 SAVEPOINT x;
@@ -87,6 +89,7 @@ SET TRANSACTION READ ONLY; -- ok
 SET TRANSACTION READ WRITE; --fail
 ERROR:  cannot set transaction read-write mode inside a read-only transaction
 COMMIT;
+ERROR:  current transaction failed, rolling back
 BEGIN;
 SET TRANSACTION READ WRITE; -- ok
 SAVEPOINT x;
@@ -271,6 +274,7 @@ ERROR:  column "trans_foo" does not exist
 LINE 1: SELECT trans_foo;
                ^
 COMMIT;
+ERROR:  current transaction failed, rolling back
 SELECT * FROM savepoints;
  a 
 ---
@@ -456,6 +460,7 @@ ERROR:  portal "c" cannot be run
 	FETCH 10 FROM c;
 ERROR:  portal "c" cannot be run
 COMMIT;
+ERROR:  current transaction failed, rolling back
 --
 -- Check that "stable" functions are really stable.  They should not be
 -- able to see the partial results of the calling query.  (Ideally we would
@@ -586,6 +591,7 @@ rollback to x;
 fetch from foo;
 ERROR:  cursor "foo" does not exist
 commit;
+ERROR:  current transaction failed, rolling back
 begin;
 create table abc (a int);
 insert into abc values (5);
@@ -657,6 +663,7 @@ FETCH ok;  -- should work
 FETCH ctt; -- must be rejected
 ERROR:  portal "ctt" cannot be run
 COMMIT;
+ERROR:  current transaction failed, rolling back
 DROP FUNCTION create_temp_tab();
 DROP FUNCTION invert(x float8);
 -- Tests for AND CHAIN
@@ -710,6 +717,7 @@ LINE 1: INSERT INTO abc VALUES ('error');
 INSERT INTO abc VALUES (3);  -- check it's really aborted
 ERROR:  current transaction is aborted, commands ignored until end of transaction block
 COMMIT AND CHAIN;  -- TBLOCK_ABORT_END
+ERROR:  current transaction failed, rolling back
 SHOW transaction_isolation;
  transaction_isolation 
 -----------------------
@@ -755,6 +763,7 @@ ERROR:  invalid input syntax for type integer: "error"
 LINE 1: INSERT INTO abc VALUES ('error');
                                 ^
 COMMIT AND CHAIN;  -- TBLOCK_ABORT_PENDING
+ERROR:  current transaction failed, rolling back
 SHOW transaction_isolation;
  transaction_isolation 
 -----------------------
diff --git a/src/test/regress/output/constraints.source b/src/test/regress/output/constraints.source
index b727c6150a..d0bcf962a8 100644
--- a/src/test/regress/output/constraints.source
+++ b/src/test/regress/output/constraints.source
@@ -541,6 +541,7 @@ INSERT INTO unique_tbl VALUES (3, 'Three'); -- should fail
 ERROR:  duplicate key value violates unique constraint "unique_tbl_i_key"
 DETAIL:  Key (i)=(3) already exists.
 COMMIT;
+ERROR:  current transaction failed, rolling back
 -- forced check when SET CONSTRAINTS is called
 BEGIN;
 SET CONSTRAINTS ALL DEFERRED;
@@ -549,6 +550,7 @@ SET CONSTRAINTS ALL IMMEDIATE; -- should fail
 ERROR:  duplicate key value violates unique constraint "unique_tbl_i_key"
 DETAIL:  Key (i)=(3) already exists.
 COMMIT;
+ERROR:  current transaction failed, rolling back
 -- test deferrable UNIQUE with a partitioned table
 CREATE TABLE parted_uniq_tbl (i int UNIQUE DEFERRABLE) partition by range (i);
 CREATE TABLE parted_uniq_tbl_1 PARTITION OF parted_uniq_tbl FOR VALUES FROM (0) TO (10);
-- 
2.20.1 (Apple Git-117)

