BUG #19782: SET CONSTRAINTS in a released savepoint survives ROLLBACK TO an enclosing savepoint

From: PG Bug reporting form <noreply(at)postgresql(dot)org>
To: pgsql-bugs(at)lists(dot)postgresql(dot)org
Cc: alpkeles99(at)gmail(dot)com
Subject: BUG #19782: SET CONSTRAINTS in a released savepoint survives ROLLBACK TO an enclosing savepoint
Date: 2026-10-21 18:25:09
Message-ID: 19782-02b66fb19c9ce85b@postgresql.org
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-bugs

The following bug has been logged on the website:

Bug reference: 19782
Logged by: Alperen Keles
Email address: alpkeles99(at)gmail(dot)com
PostgreSQL version: 14.24
Operating system: macOS 26 arm64
Description:

Hello,

SET CONSTRAINTS run in a savepoint is undone by ROLLBACK TO that
savepoint. But if it runs in an inner savepoint that is then released,
a ROLLBACK TO the enclosing savepoint does not undo it, and the
constraints keep the mode the rolled-back command gave them.

CREATE TABLE p (k int PRIMARY KEY);
CREATE TABLE c (f int CONSTRAINT c_f REFERENCES p DEFERRABLE);

BEGIN;
SAVEPOINT s0;
SAVEPOINT s1;
SET CONSTRAINTS ALL DEFERRED;
RELEASE s1;
ROLLBACK TO s0;
INSERT INTO c VALUES (99); -- INSERT 0 1: c_f is still deferred
COMMIT;
ERROR: insert or update on table "c" violates foreign key constraint
"c_f"
DETAIL: Key (f)=(99) is not present in table "p".

Without the inner savepoint the INSERT fails right away, as expected:

BEGIN;
SAVEPOINT s0;
SET CONSTRAINTS ALL DEFERRED;
ROLLBACK TO s0;
INSERT INTO c VALUES (99);
ERROR: insert or update on table "c" violates foreign key constraint
"c_f"

RELEASE SAVEPOINT merges s1's changes into s0, and ROLLBACK TO s0 rolls
back all commands executed after s0 was established, so the two cases
should behave the same: c_f should be immediate again after ROLLBACK TO
s0.

It works in the other direction too. With a DEFERRABLE INITIALLY
DEFERRED foreign key, SET CONSTRAINTS ALL IMMEDIATE in a released inner
savepoint survives ROLLBACK TO s0, so a transaction that inserts the
child row before the parent row fails at the first INSERT even though
it would commit without the savepoints:

CREATE TABLE d (f int CONSTRAINT d_f REFERENCES p
DEFERRABLE INITIALLY DEFERRED);
BEGIN;
SAVEPOINT s0;
SAVEPOINT s1;
SET CONSTRAINTS ALL IMMEDIATE;
RELEASE s1;
ROLLBACK TO s0;
INSERT INTO d VALUES (5);
ERROR: insert or update on table "d" violates foreign key constraint
"d_f"
INSERT INTO p VALUES (5);
ERROR: current transaction is aborted, commands ignored until end of
transaction block

The same happens with SET CONSTRAINTS by name, with deferrable UNIQUE
and EXCLUDE constraints, at deeper nesting (the SET in s2, RELEASE s2,
then ROLLBACK TO s1 or s0), and with PL/pgSQL exception blocks, which
are subtransactions too: an inner block that runs SET CONSTRAINTS ALL
DEFERRED and exits normally, inside an outer block whose exception is
caught, leaves the constraints deferred; so does calling a function
whose exception block runs SET CONSTRAINTS inside a savepoint that is
then rolled back. A ROLLBACK of the whole transaction is not affected.

I could not get a violating row committed this way: the wrongly
deferred check still runs at COMMIT, which fails. The effect is that
the check happens at the wrong statement, so an error that the
application expects to catch at the INSERT (with a savepoint or an
exception handler) aborts the whole transaction at COMMIT instead, or,
in the IMMEDIATE direction, a valid transaction is rejected.

Reproduced on 14.24 (REL_14_STABLE b1c44848ce3), 18.6 (REL_18_STABLE
a85b5523e67), 19rc1 (REL_19_STABLE 0fb192f1b02) and master dd568b047ac
(2026-10-21),
macOS arm64, all --enable-cassert.

As far as I can tell, the cause is in trigger.c. The SET CONSTRAINTS
state is saved for a subtransaction level only when SET CONSTRAINTS
runs at that level (AfterTriggerSetState copies it into
trans_stack[my_level].state). When the inner level commits,
AfterTriggerEndSubXact frees its saved copy ("If we saved a prior
state, we don't need it anymore") and does not pass it to the parent
level, whose slot is still NULL. So when the parent level aborts, it
finds nothing to restore ("If the saved state is NULL, then this
subxact didn't save it, so it doesn't need restoring"). The code
seems to date from the nested transactions commit (573a71a5da7, 8.0).

Found with a query fuzzer.

Regards,
Alperen Keles

Browse pgsql-bugs by date

  From Date Subject
Next Message Nitin Motiani 2026-10-21 18:39:31 Re: BUG #19773: pg_dump omits a different generation expression of an inherited column in a child table
Previous Message PG Bug reporting form 2026-10-21 18:23:49 BUG #19781: UPDATE moving a row of a self-referencing partitioned table fails with 23503 although ON UPDATE CASC