public inbox for [email protected]
help / color / mirror / Atom feedFrom: Yasuo Honda <[email protected]>
To: PostgreSQL-development <[email protected]>
Subject: [PATCH] Fix unexpected loss of DEFERRABLE property after toggling NOT ENFORCED / ENFORCED
Date: Fri, 27 Feb 2026 18:51:02 +0900
Message-ID: <CAKmOUTms2nkxEZDdcrsjq5P3b2L_PR266Hv8kW5pANwmVaRJJQ@mail.gmail.com> (raw)
Hi,
I have encountered an unexpected behavior where the DEFERRABLE and
INITIALLY DEFERRED properties of foreign keys are lost after toggling
them from NOT ENFORCED to ENFORCED.
Background
In the Ruby on Rails framework, there is a built-in mechanism to
temporarily bypass foreign key checks while loading test data.
Currently, this is implemented using: ALTER TABLE ... DISABLE TRIGGER
ALL; ALTER TABLE ... ENABLE TRIGGER ALL;
However, this requires superuser privileges. With the newly introduced
support for "NOT ENFORCED" foreign keys in PostgreSQL 18, I am
interested in switching to: ALTER TABLE ... ALTER CONSTRAINT ... NOT
ENFORCED; ALTER TABLE ... ALTER CONSTRAINT ... ENFORCED;
This would allow the operation to be performed by the table owner
without superuser rights. However, I discovered that switching the
state back to ENFORCED unexpectedly strips away the DEFERRABLE
property.
Problem
When re-enforcing a constraint, there is a discrepancy between the
constraint definition and its underlying triggers.
While the flags in pg_constraint remain correct, the corresponding
triggers in pg_trigger (tgdeferrable and tginitdeferred) are reset to
defaults ('f') when they are reconstructed during the ENFORCED
operation.
I have attached a reproduction SQL script that demonstrates this by
comparing the values in pg_constraint and pg_trigger. In the current
PostgreSQL 18.3, you can see that the triggers lose their
deferrability even though the constraint itself is still defined as
DEFERRABLE. This causes "SET CONSTRAINTS ... DEFERRED" to fail.
After Patch
* The tgdeferrable and tginitdeferred flags in pg_trigger are
correctly preserved to match pg_constraint after the toggle, and
deferred execution works as expected.
I've attached the reproduction SQL script and a patch to fix this in
src/backend/commands/tablecmds.c. The patch and reproduction SQL
script were developed with the assistance of Claude Code. I have
reviewed and verified the code myself.
Any feedback is appreciated.
Regards,
--
Yasuo Honda
Attachments:
[application/octet-stream] 0001-Restore-tgdeferrable-and-tginitdeferred-after-NOT-EN.patch (3.3K, 2-0001-Restore-tgdeferrable-and-tginitdeferred-after-NOT-EN.patch)
download | inline diff:
From a061bdd232ee96302c0af4d4a7f4c0f901262ca9 Mon Sep 17 00:00:00 2001
From: Yasuo Honda <[email protected]>
Date: Fri, 27 Feb 2026 17:30:04 +0900
Subject: [PATCH] Restore tgdeferrable and tginitdeferred after NOT ENFORCED
then ENFORCED
---
src/backend/commands/tablecmds.c | 2 ++
src/test/regress/expected/foreign_key.out | 18 ++++++++++++++++++
src/test/regress/sql/foreign_key.sql | 10 ++++++++++
3 files changed, 30 insertions(+)
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index b04b0dbd2a0..bc07dfe2ce8 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12501,6 +12501,8 @@ ATExecAlterConstrEnforceability(List **wqueue, ATAlterConstraint *cmdcon,
fkconstraint->fk_matchtype = currcon->confmatchtype;
fkconstraint->fk_upd_action = currcon->confupdtype;
fkconstraint->fk_del_action = currcon->confdeltype;
+ fkconstraint->deferrable = currcon->condeferrable;
+ fkconstraint->initdeferred = currcon->condeferred;
/* Create referenced triggers */
if (currcon->conrelid == fkrelid)
diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out
index 9ae4dbf1b0a..d061e51da6b 100644
--- a/src/test/regress/expected/foreign_key.out
+++ b/src/test/regress/expected/foreign_key.out
@@ -1349,6 +1349,24 @@ UPDATE pktable SET id = 10 WHERE id = 5;
-- doesn't match PK, but no error.
INSERT INTO fktable VALUES (0, 20);
ROLLBACK;
+-- verify that tgdeferrable/tginitdeferred are preserved after NOT ENFORCED -> ENFORCED
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey ENFORCED;
+SELECT tgdeferrable, tginitdeferred FROM pg_trigger
+WHERE tgconstraint = (SELECT oid FROM pg_constraint
+ WHERE conrelid = 'fktable'::regclass
+ AND conname = 'fktable_fk_fkey');
+ tgdeferrable | tginitdeferred
+--------------+----------------
+ t | t
+ t | t
+ t | t
+ t | t
+(4 rows)
+
+-- reset
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- illegal options
diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql
index 3b8c95bf893..0afed60f688 100644
--- a/src/test/regress/sql/foreign_key.sql
+++ b/src/test/regress/sql/foreign_key.sql
@@ -1023,6 +1023,16 @@ UPDATE pktable SET id = 10 WHERE id = 5;
INSERT INTO fktable VALUES (0, 20);
ROLLBACK;
+-- verify that tgdeferrable/tginitdeferred are preserved after NOT ENFORCED -> ENFORCED
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey DEFERRABLE INITIALLY DEFERRED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey NOT ENFORCED;
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey ENFORCED;
+SELECT tgdeferrable, tginitdeferred FROM pg_trigger
+WHERE tgconstraint = (SELECT oid FROM pg_constraint
+ WHERE conrelid = 'fktable'::regclass
+ AND conname = 'fktable_fk_fkey');
+-- reset
+ALTER TABLE FKTABLE ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
-- try additional syntax
ALTER TABLE fktable ALTER CONSTRAINT fktable_fk_fkey NOT DEFERRABLE;
--
2.53.0
[application/octet-stream] tgdeferrable-tginitdeferred-not-restored.sql (1.5K, 3-tgdeferrable-tginitdeferred-not-restored.sql)
download
view thread (6+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected]
Subject: Re: [PATCH] Fix unexpected loss of DEFERRABLE property after toggling NOT ENFORCED / ENFORCED
In-Reply-To: <CAKmOUTms2nkxEZDdcrsjq5P3b2L_PR266Hv8kW5pANwmVaRJJQ@mail.gmail.com>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox