From a493d64059e95022cbad0efd0d89bd941c22f74c Mon Sep 17 00:00:00 2001 From: Patrick Reynolds 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