From 776d3b2cc4cc7852e546a53d3eb24827558319db Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Mon, 31 Aug 2026 12:00:35 +0000 Subject: [PATCH 2/2] Disallow foreign keys referencing unlogged partitions The foreign key persistence checks are applied only to the referenced relation named in the constraint. When that relation is partitioned, it is always permanent, but unlogged partitions may exist below it, and neither the referenced-side recursion nor the cloning performed when a partition is created or attached re-checks persistence. A permanent table could therefore reference rows in an unlogged partition. After a crash, recovery resets the partition to its init fork: the referenced rows vanish while the referencing rows remain, leaving the constraint durably violated, and a dump of the database can no longer be restored. Of these routes, only ALTER TABLE ... SET UNLOGGED on the partition was blocked. To fix, repeat the persistence check in addFkRecurseReferenced, which every relation on the referenced side passes through when a constraint is created or cloned for a newly created or attached partition. Only the permanent-to-unlogged combination needs to be rejected there, since temporary relations cannot be partitions of permanent ones. The bug is as old as foreign keys referencing partitioned tables, in v12. --- src/backend/commands/tablecmds.c | 21 ++++++++++++ src/test/regress/expected/foreign_key.out | 42 +++++++++++++++++++++++ src/test/regress/sql/foreign_key.sql | 34 ++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c index fd144d783d9..41a76835177 100644 --- a/src/backend/commands/tablecmds.c +++ b/src/backend/commands/tablecmds.c @@ -11010,6 +11010,27 @@ addFkRecurseReferenced(Constraint *fkconstraint, Relation rel, Assert(CheckRelationLockedByMe(pkrel, ShareRowExclusiveLock, true)); Assert(CheckRelationLockedByMe(rel, ShareRowExclusiveLock, true)); + /* + * ATAddForeignKeyConstraint checks the persistence of the referenced + * relation named in the constraint, but that says nothing about the + * partitions below it: a permanent partitioned table may contain + * unlogged partitions, whose data vanishes on crash recovery just as for + * a standalone unlogged table. Every relation on the referenced side + * passes through here, both when the constraint is created and when it + * is cloned for a newly created or attached partition, so repeat the + * check for each of them. Only the permanent-to-unlogged combination + * needs to be considered, because temporary relations cannot be + * partitions of permanent ones. + */ + if (rel->rd_rel->relpersistence == RELPERSISTENCE_PERMANENT && + !RelationIsPermanent(pkrel)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_TABLE_DEFINITION), + errmsg("constraints on permanent tables may reference only permanent tables"), + errdetail("Table \"%s\" is an unlogged partition of a table referenced by permanent table \"%s\".", + RelationGetRelationName(pkrel), + RelationGetRelationName(rel)))); + /* * Create action triggers to enforce the constraint, or skip them if the * constraint is NOT ENFORCED. diff --git a/src/test/regress/expected/foreign_key.out b/src/test/regress/expected/foreign_key.out index ac044eb40fa..0ec15de5df3 100644 --- a/src/test/regress/expected/foreign_key.out +++ b/src/test/regress/expected/foreign_key.out @@ -4113,3 +4113,45 @@ SELECT count(*) AS deferred_rows FROM fp_deferred_fk; -- 1, check passed at com (1 row) DROP TABLE fp_deferred_fk, fp_deferred_pk; +-- FK persistence rules apply to unlogged partitions on the referenced side +-- just like to standalone unlogged tables: crash recovery resets them, so +-- rows in a permanent referencing table would be left dangling. +CREATE SCHEMA fkpart14; +SET search_path TO fkpart14; +CREATE TABLE pk (a int PRIMARY KEY) PARTITION BY RANGE (a); +CREATE UNLOGGED TABLE pk1 PARTITION OF pk FOR VALUES FROM (0) TO (100); +-- fails: an existing partition of the referenced table is unlogged +CREATE TABLE fk (x int REFERENCES pk); +ERROR: constraints on permanent tables may reference only permanent tables +DETAIL: Table "pk1" is an unlogged partition of a table referenced by permanent table "fk". +-- ok: an unlogged table may reference unlogged partitions +CREATE UNLOGGED TABLE ufk (x int REFERENCES pk); +DROP TABLE ufk, pk; +CREATE TABLE pk (a int PRIMARY KEY) PARTITION BY RANGE (a); +CREATE TABLE fk (x int REFERENCES pk); +-- fails: creating an unlogged partition of the referenced table +CREATE UNLOGGED TABLE pk1 PARTITION OF pk FOR VALUES FROM (0) TO (100); +ERROR: constraints on permanent tables may reference only permanent tables +DETAIL: Table "pk1" is an unlogged partition of a table referenced by permanent table "fk". +-- fails: attaching an unlogged partition to the referenced table +CREATE UNLOGGED TABLE pk2 (a int PRIMARY KEY); +ALTER TABLE pk ATTACH PARTITION pk2 FOR VALUES FROM (0) TO (100); +ERROR: constraints on permanent tables may reference only permanent tables +DETAIL: Table "pk2" is an unlogged partition of a table referenced by permanent table "fk". +-- fails: same, with the unlogged table below a sub-partitioned table +CREATE TABLE pk3 (a int PRIMARY KEY) PARTITION BY RANGE (a); +CREATE UNLOGGED TABLE pk31 PARTITION OF pk3 FOR VALUES FROM (0) TO (50); +ALTER TABLE pk ATTACH PARTITION pk3 FOR VALUES FROM (0) TO (100); +ERROR: constraints on permanent tables may reference only permanent tables +DETAIL: Table "pk31" is an unlogged partition of a table referenced by permanent table "fk". +-- ok: permanent partitions can be added, but not made unlogged afterwards +CREATE TABLE pk4 PARTITION OF pk FOR VALUES FROM (0) TO (100); +ALTER TABLE pk4 SET UNLOGGED; +ERROR: could not change table "pk4" to unlogged because it references logged table "fk" +-- ok: unlogged partitions on the referencing side are still allowed +CREATE TABLE pk5 (a int PRIMARY KEY); +CREATE TABLE fk2 (x int REFERENCES pk5) PARTITION BY RANGE (x); +CREATE UNLOGGED TABLE fk21 PARTITION OF fk2 FOR VALUES FROM (0) TO (100); +DROP TABLE fk2, pk5, fk, pk, pk2, pk3; +DROP SCHEMA fkpart14; +RESET search_path; diff --git a/src/test/regress/sql/foreign_key.sql b/src/test/regress/sql/foreign_key.sql index a93e81b42bc..776f627fef0 100644 --- a/src/test/regress/sql/foreign_key.sql +++ b/src/test/regress/sql/foreign_key.sql @@ -3032,3 +3032,37 @@ INSERT INTO fp_deferred_pk VALUES (1); COMMIT; SELECT count(*) AS deferred_rows FROM fp_deferred_fk; -- 1, check passed at commit DROP TABLE fp_deferred_fk, fp_deferred_pk; + +-- FK persistence rules apply to unlogged partitions on the referenced side +-- just like to standalone unlogged tables: crash recovery resets them, so +-- rows in a permanent referencing table would be left dangling. +CREATE SCHEMA fkpart14; +SET search_path TO fkpart14; +CREATE TABLE pk (a int PRIMARY KEY) PARTITION BY RANGE (a); +CREATE UNLOGGED TABLE pk1 PARTITION OF pk FOR VALUES FROM (0) TO (100); +-- fails: an existing partition of the referenced table is unlogged +CREATE TABLE fk (x int REFERENCES pk); +-- ok: an unlogged table may reference unlogged partitions +CREATE UNLOGGED TABLE ufk (x int REFERENCES pk); +DROP TABLE ufk, pk; +CREATE TABLE pk (a int PRIMARY KEY) PARTITION BY RANGE (a); +CREATE TABLE fk (x int REFERENCES pk); +-- fails: creating an unlogged partition of the referenced table +CREATE UNLOGGED TABLE pk1 PARTITION OF pk FOR VALUES FROM (0) TO (100); +-- fails: attaching an unlogged partition to the referenced table +CREATE UNLOGGED TABLE pk2 (a int PRIMARY KEY); +ALTER TABLE pk ATTACH PARTITION pk2 FOR VALUES FROM (0) TO (100); +-- fails: same, with the unlogged table below a sub-partitioned table +CREATE TABLE pk3 (a int PRIMARY KEY) PARTITION BY RANGE (a); +CREATE UNLOGGED TABLE pk31 PARTITION OF pk3 FOR VALUES FROM (0) TO (50); +ALTER TABLE pk ATTACH PARTITION pk3 FOR VALUES FROM (0) TO (100); +-- ok: permanent partitions can be added, but not made unlogged afterwards +CREATE TABLE pk4 PARTITION OF pk FOR VALUES FROM (0) TO (100); +ALTER TABLE pk4 SET UNLOGGED; +-- ok: unlogged partitions on the referencing side are still allowed +CREATE TABLE pk5 (a int PRIMARY KEY); +CREATE TABLE fk2 (x int REFERENCES pk5) PARTITION BY RANGE (x); +CREATE UNLOGGED TABLE fk21 PARTITION OF fk2 FOR VALUES FROM (0) TO (100); +DROP TABLE fk2, pk5, fk, pk, pk2, pk3; +DROP SCHEMA fkpart14; +RESET search_path; -- 2.55.0