From df6338cfd9d6314ebc214ac58f1b0ed3fcc27c33 Mon Sep 17 00:00:00 2001 From: Amit Langote Date: Wed, 7 Oct 2026 21:45:19 +0900 Subject: [PATCH v2 1/2] Refuse RI fast-path row locks in read-only transactions The SPI path checks a foreign key with SELECT ... FOR KEY SHARE, which ExecCheckXactReadOnly() refuses in a read-only transaction unless the referenced table is temporary. The fast path locks the referenced row without that check, so for example a deferred check that fires at COMMIT after SET TRANSACTION READ ONLY succeeded, having locked a row in a read-only transaction, where the SPI path fails with "cannot execute SELECT FOR KEY SHARE in a read-only transaction". Fix the fast path to refuse it the same way so the behavior matches with checks going through SPI. Discussion: https://postgr.es/m/CA+HiwqG79XK1oObdZ2AwT660CeJ6s3Mn4LrFPCme-k4L2rF_ag@mail.gmail.com Backpatch-through: 19 --- src/backend/utils/adt/ri_triggers.c | 10 ++++++++ src/test/regress/expected/foreign_key.out | 31 +++++++++++++++++++++++ src/test/regress/sql/foreign_key.sql | 21 +++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/src/backend/utils/adt/ri_triggers.c b/src/backend/utils/adt/ri_triggers.c index 2e7a815105f..d588a21b65a 100644 --- a/src/backend/utils/adt/ri_triggers.c +++ b/src/backend/utils/adt/ri_triggers.c @@ -31,6 +31,7 @@ #include "access/tableam.h" #include "access/xact.h" #include "catalog/index.h" +#include "catalog/namespace.h" #include "catalog/objectaccess.h" #include "catalog/pg_am_d.h" #include "catalog/pg_collation.h" @@ -45,6 +46,7 @@ #include "miscadmin.h" #include "parser/parse_coerce.h" #include "parser/parse_relation.h" +#include "tcop/utility.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/datum.h" @@ -2782,6 +2784,14 @@ ri_FastPathCheck(RI_ConstraintInfo *riinfo, pk_rel = table_open(riinfo->pk_relid, RowShareLock); + /* + * The row lock taken below is refused in a read-only transaction unless + * the referenced table is temporary, as ExecCheckXactReadOnly() refuses + * the SPI path's SELECT ... FOR KEY SHARE. + */ + if (XactReadOnly && !isTempNamespace(RelationGetNamespace(pk_rel))) + PreventCommandIfReadOnly("SELECT FOR KEY SHARE"); + /* * Advance the command counter so the check sees the effects of prior * triggers in this statement, as SPI does when executing the query issued diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index 4d7cfc31c60..5532af91236 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -4206,3 +4206,34 @@ ERROR: insert or update on table "ri_fk_io" violates foreign key constraint "ri DETAIL: Key (a)=(2) is not present in table "ri_pk". ROLLBACK TO SAVEPOINT s; ROLLBACK; +-- An FK check locks the referenced row, which a read-only transaction does +-- not allow unless the referenced table is temporary. +CREATE TABLE fp_pk_ro (a int PRIMARY KEY); +INSERT INTO fp_pk_ro VALUES (1); +CREATE TABLE fp_fk_ro (a int REFERENCES fp_pk_ro DEFERRABLE INITIALLY DEFERRED); +BEGIN; +INSERT INTO fp_fk_ro VALUES (1); +SET TRANSACTION READ ONLY; +COMMIT; -- fails +ERROR: cannot execute SELECT FOR KEY SHARE in a read-only transaction +CREATE TEMP TABLE fp_pk_ro_tmp (a int PRIMARY KEY); +INSERT INTO fp_pk_ro_tmp VALUES (1); +CREATE TEMP TABLE fp_fk_ro_tmp (a int REFERENCES fp_pk_ro_tmp + DEFERRABLE INITIALLY DEFERRED); +BEGIN; +INSERT INTO fp_fk_ro_tmp VALUES (1); +SET TRANSACTION READ ONLY; +COMMIT; -- succeeds +SELECT count(*) FROM fp_fk_ro; + count +------- + 0 +(1 row) + +SELECT count(*) FROM fp_fk_ro_tmp; + count +------- + 1 +(1 row) + +DROP TABLE fp_fk_ro, fp_pk_ro, fp_fk_ro_tmp, fp_pk_ro_tmp; diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index 4104e13a04b..55477fe4e7e 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -3145,3 +3145,24 @@ SAVEPOINT s; INSERT INTO ri_fk_io VALUES ('2'); -- fails ROLLBACK TO SAVEPOINT s; ROLLBACK; + +-- An FK check locks the referenced row, which a read-only transaction does +-- not allow unless the referenced table is temporary. +CREATE TABLE fp_pk_ro (a int PRIMARY KEY); +INSERT INTO fp_pk_ro VALUES (1); +CREATE TABLE fp_fk_ro (a int REFERENCES fp_pk_ro DEFERRABLE INITIALLY DEFERRED); +BEGIN; +INSERT INTO fp_fk_ro VALUES (1); +SET TRANSACTION READ ONLY; +COMMIT; -- fails +CREATE TEMP TABLE fp_pk_ro_tmp (a int PRIMARY KEY); +INSERT INTO fp_pk_ro_tmp VALUES (1); +CREATE TEMP TABLE fp_fk_ro_tmp (a int REFERENCES fp_pk_ro_tmp + DEFERRABLE INITIALLY DEFERRED); +BEGIN; +INSERT INTO fp_fk_ro_tmp VALUES (1); +SET TRANSACTION READ ONLY; +COMMIT; -- succeeds +SELECT count(*) FROM fp_fk_ro; +SELECT count(*) FROM fp_fk_ro_tmp; +DROP TABLE fp_fk_ro, fp_pk_ro, fp_fk_ro_tmp, fp_pk_ro_tmp; -- 2.47.3