Fix detach of a partition that has a toplevel FK to a partitioned table
authorÁlvaro Herrera <alvherre@alvh.no-ip.org>
Tue, 21 Jan 2025 13:53:46 +0000 (14:53 +0100)
committerÁlvaro Herrera <alvherre@alvh.no-ip.org>
Tue, 21 Jan 2025 13:53:46 +0000 (14:53 +0100)
In common cases, foreign keys are defined on the toplevel partitioned
table; but if instead one is defined on a partition and references a
partitioned table, and the referencing partition is detached, we would
examine the pg_constraint row on the partition being detached, and fail
to realize that the sub-constraints must be left alone.  This causes the
ALTER TABLE DETACH process to fail with

 ERROR:  could not find ON INSERT check triggers of foreign key constraint NNN

This is similar but not quite the same as what was fixed by
53af9491a043.  This bug doesn't affect branches earlier than 15, because
the detach procedure was different there, so we only backpatch down to
15.

Fix by skipping such modifying constraints that are children of other
constraints being detached.

Author: Amul Sul <sulamul@gmail.com>
Diagnosys-by: Sami Imseih <samimseih@gmail.com>
Discussion: https://postgr.es/m/CAAJ_b97GuPh6wQPbxQS-Zpy16Oh+0aMv-w64QcGrLhCOZZ6p+g@mail.gmail.com

src/backend/commands/tablecmds.c
src/test/regress/expected/foreign_key.out
src/test/regress/sql/foreign_key.sql

index 4117a0ab1a60c2e005e3a0d5730b7a5d71e61c28..d8f0a99ad9388e7dd95127207b90d3096d6aae8e 100644 (file)
@@ -19987,6 +19987,7 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
    HeapTuple   tuple,
                newtuple;
    Relation    trigrel = NULL;
+   List       *fkoids = NIL;
 
    if (concurrent)
    {
@@ -20007,6 +20008,23 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
    fks = copyObject(RelationGetFKeyList(partRel));
    if (fks != NIL)
        trigrel = table_open(TriggerRelationId, RowExclusiveLock);
+
+   /*
+    * It's possible that the partition being detached has a foreign key that
+    * references a partitioned table.  When that happens, there are multiple
+    * pg_constraint rows for the partition: one points to the partitioned
+    * table itself, while the others point to each of its partitions.  Only
+    * the topmost one is to be considered here; the child constraints must be
+    * left alone, because conceptually those aren't coming from our parent
+    * partitioned table, but from this partition itself.
+    *
+    * We implement this by collecting all the constraint OIDs in a first scan
+    * of the FK array, and skipping in the loop below those constraints whose
+    * parents are listed here.
+    */
+   foreach_node(ForeignKeyCacheInfo, fk, fks)
+       fkoids = lappend_oid(fkoids, fk->conoid);
+
    foreach(cell, fks)
    {
        ForeignKeyCacheInfo *fk = lfirst(cell);
@@ -20020,9 +20038,13 @@ DetachPartitionFinalize(Relation rel, Relation partRel, bool concurrent,
            elog(ERROR, "cache lookup failed for constraint %u", fk->conoid);
        conform = (Form_pg_constraint) GETSTRUCT(contup);
 
-       /* consider only the inherited foreign keys */
+       /*
+        * Consider only inherited foreign keys, and only if their parents
+        * aren't in the list.
+        */
        if (conform->contype != CONSTRAINT_FOREIGN ||
-           !OidIsValid(conform->conparentid))
+           !OidIsValid(conform->conparentid) ||
+           list_member_oid(fkoids, conform->conparentid))
        {
            ReleaseSysCache(contup);
            continue;
index 3f459f70ac1e63a2a0d4552a7801fc193908589a..ee4cf85fda908825c2d7a8006263e9fe1cb96f2a 100644 (file)
@@ -2351,6 +2351,13 @@ UPDATE pk SET a = 4002 WHERE a = 4000;
 DELETE FROM pk WHERE a = 4002;
 UPDATE pk SET a = 4502 WHERE a = 4500;
 DELETE FROM pk WHERE a = 4502;
+-- Also, detaching a partition that has the FK itself should work
+-- https://postgr.es/m/CAAJ_b97GuPh6wQPbxQS-Zpy16Oh+0aMv-w64QcGrLhCOZZ6p+g@mail.gmail.com
+CREATE TABLE ffk (a int, b int REFERENCES pk) PARTITION BY list (a);
+CREATE TABLE ffk1 PARTITION OF ffk FOR VALUES IN (1);
+ALTER TABLE ffk1 ADD FOREIGN KEY (a) REFERENCES pk;
+ALTER TABLE ffk DETACH PARTITION ffk1;
+DROP TABLE ffk, ffk1;
 CREATE SCHEMA fkpart4;
 SET search_path TO fkpart4;
 -- dropping/detaching PARTITIONs is prevented if that would break
index 2e710e419c2c6f6b9635023255ee0b24b7f128fb..8c4e4c7c83358de4920118e7bc65bf2a6c6d7769 100644 (file)
@@ -1672,6 +1672,14 @@ DELETE FROM pk WHERE a = 4002;
 UPDATE pk SET a = 4502 WHERE a = 4500;
 DELETE FROM pk WHERE a = 4502;
 
+-- Also, detaching a partition that has the FK itself should work
+-- https://postgr.es/m/CAAJ_b97GuPh6wQPbxQS-Zpy16Oh+0aMv-w64QcGrLhCOZZ6p+g@mail.gmail.com
+CREATE TABLE ffk (a int, b int REFERENCES pk) PARTITION BY list (a);
+CREATE TABLE ffk1 PARTITION OF ffk FOR VALUES IN (1);
+ALTER TABLE ffk1 ADD FOREIGN KEY (a) REFERENCES pk;
+ALTER TABLE ffk DETACH PARTITION ffk1;
+DROP TABLE ffk, ffk1;
+
 CREATE SCHEMA fkpart4;
 SET search_path TO fkpart4;
 -- dropping/detaching PARTITIONs is prevented if that would break