From a493d64059e95022cbad0efd0d89bd941c22f74c Mon Sep 17 00:00:00 2001
From: Patrick Reynolds <patrick@piki.org>
Date: Thu, 13 Aug 2026 12:45:32 -0400
Subject: [PATCH v2] Fix failing assert in deferred constraint trigger

This SQL crashes assert-enabled builds:

    CREATE TABLE t(a int);
    CREATE FUNCTION f() RETURNS trigger LANGUAGE plpgsql AS $$
    BEGIN
        BEGIN
            PERFORM 1 / 0;
        EXCEPTION WHEN division_by_zero THEN
            NULL;
        END;
        RETURN NEW;
    END
    $$;
    CREATE CONSTRAINT TRIGGER trg
    AFTER INSERT ON t
    DEFERRABLE INITIALLY DEFERRED
    FOR EACH ROW EXECUTE FUNCTION f();
    BEGIN;
    INSERT INTO t VALUES (1);
    COMMIT;  -- backend aborts here on assert builds

The failure in the logs is:

    TRAP: failed Assert("s->blockState == TBLOCK_SUBINPROGRESS || s->blockState
    == TBLOCK_INPROGRESS || s->blockState == TBLOCK_IMPLICIT_INPROGRESS ||
    s->blockState == TBLOCK_PARALLEL_INPROGRESS || s->blockState ==
    TBLOCK_STARTED"), File: "xact.c", Line: 4851, PID: 73455
    0   postgres                 0x0000000104e6b330 ExceptionalCondition + 108
    1   postgres                 0x0000000104a33620 AbortSubTransaction + 0
    2   plpgsql.dylib            0x00000001056b14b8 exec_stmt_block + 640
    3   plpgsql.dylib            0x00000001056b1d00 exec_stmts + 188

`PREPARE TRANSACTION 'tx'` in place of the final `COMMIT` crashes in the
same way.

fa0e318f947 updated BeginInternalSubTransaction to allow creating a
subtransaction with a parent of TBLOCK_END and TBLOCK_PREPARE but didn't
add those states to the assert in RollbackAndReleaseCurrentSubTransaction.

The `division_by_zero` exception in the example forces a subtransaction to
abort in the context of the final `COMMIT`, so
RollbackAndReleaseCurrentSubTransaction needs to allow TBLOCK_PREPARE and
TBLOCK_END.

Now it does.

This problem only affects assert-enabled builds, possibly as far back as
8.2.  I've personally confirmed the problem on 18.4 and 17.10.  The assert
is the only thing wrong here; adding the two additional parent states to
it doesn't allow through any behavior that we didn't intend.
---
 src/backend/access/transam/xact.c      |  2 ++
 src/test/regress/expected/triggers.out | 28 ++++++++++++++++++++++++++
 src/test/regress/sql/triggers.sql      | 25 +++++++++++++++++++++++
 3 files changed, 55 insertions(+)

diff --git a/src/backend/access/transam/xact.c b/src/backend/access/transam/xact.c
index aca92507ebd..ebb010853cf 100644
--- a/src/backend/access/transam/xact.c
+++ b/src/backend/access/transam/xact.c
@@ -4901,6 +4901,8 @@ RollbackAndReleaseCurrentSubTransaction(void)
 		   s->blockState == TBLOCK_INPROGRESS ||
 		   s->blockState == TBLOCK_IMPLICIT_INPROGRESS ||
 		   s->blockState == TBLOCK_PARALLEL_INPROGRESS ||
+		   s->blockState == TBLOCK_END ||
+		   s->blockState == TBLOCK_PREPARE ||
 		   s->blockState == TBLOCK_STARTED);
 }
 
diff --git a/src/test/regress/expected/triggers.out b/src/test/regress/expected/triggers.out
index 8fcb33ac81a..c3ecd9095e9 100644
--- a/src/test/regress/expected/triggers.out
+++ b/src/test/regress/expected/triggers.out
@@ -2308,6 +2308,34 @@ create constraint trigger crtr
 ERROR:  constraint triggers cannot be marked NOT ENFORCED
 LINE 2:   after insert on foo not enforced
                               ^
+-- Test exception handling in a deferred constraint trigger at COMMIT.
+create table deferred_trigger_test (a int);
+create function deferred_trigger_func() returns trigger
+  language plpgsql as $$
+begin
+  perform 1 / 0;
+  return new;
+exception when division_by_zero then
+  raise notice 'caught division_by_zero';
+  return new;
+end;
+$$;
+create constraint trigger deferred_trigger
+  after insert on deferred_trigger_test
+  deferrable initially deferred
+  for each row execute function deferred_trigger_func();
+begin;
+insert into deferred_trigger_test values (1);
+commit;
+NOTICE:  caught division_by_zero
+select * from deferred_trigger_test;
+ a 
+---
+ 1
+(1 row)
+
+drop table deferred_trigger_test;
+drop function deferred_trigger_func();
 --
 -- Constraint triggers and partitioned tables
 create table parted_constr_ancestor (a int, b text)
diff --git a/src/test/regress/sql/triggers.sql b/src/test/regress/sql/triggers.sql
index 2285e90110e..e7f1cd823dc 100644
--- a/src/test/regress/sql/triggers.sql
+++ b/src/test/regress/sql/triggers.sql
@@ -1590,6 +1590,31 @@ create constraint trigger crtr
   after insert on foo not enforced
   for each row execute procedure foo ();
 
+-- Test exception handling in a deferred constraint trigger at COMMIT.
+create table deferred_trigger_test (a int);
+create function deferred_trigger_func() returns trigger
+  language plpgsql as $$
+begin
+  perform 1 / 0;
+  return new;
+exception when division_by_zero then
+  raise notice 'caught division_by_zero';
+  return new;
+end;
+$$;
+create constraint trigger deferred_trigger
+  after insert on deferred_trigger_test
+  deferrable initially deferred
+  for each row execute function deferred_trigger_func();
+
+begin;
+insert into deferred_trigger_test values (1);
+commit;
+select * from deferred_trigger_test;
+
+drop table deferred_trigger_test;
+drop function deferred_trigger_func();
+
 --
 -- Constraint triggers and partitioned tables
 create table parted_constr_ancestor (a int, b text)
-- 
2.55.0

