Fix incorrect logic for caching ResultRelInfos for triggers REL_15_STABLE github/REL_15_STABLE
authorDavid Rowley <drowley@postgresql.org>
Sat, 25 Oct 2025 22:02:36 +0000 (11:02 +1300)
committerDavid Rowley <drowley@postgresql.org>
Sat, 25 Oct 2025 22:02:36 +0000 (11:02 +1300)
When dealing with ResultRelInfos for partitions, there are cases where
there are mixed requirements for the ri_RootResultRelInfo.  There are
cases when the partition itself requires a NULL ri_RootResultRelInfo and
in the same query, the same partition may require a ResultRelInfo with
its parent set in ri_RootResultRelInfo.  This could cause the column
mapping between the partitioned table and the partition not to be done
which could result in crashes if the column attnums didn't match
exactly.

The fix is simple.  We now check that the ri_RootResultRelInfo matches
what the caller passed to ExecGetTriggerResultRel() and only return a
cached ResultRelInfo when the ri_RootResultRelInfo matches what the
caller wants, otherwise we'll make a new one.

Author: David Rowley <dgrowleyml@gmail.com>
Author: Amit Langote <amitlangote09@gmail.com>
Reported-by: Dmitry Fomin <fomin.list@gmail.com>
Discussion: https://postgr.es/m/7DCE78D7-0520-4207-822B-92F60AEA14B4@gmail.com
Backpatch-through: 15

src/backend/executor/execMain.c
src/test/regress/expected/foreign_key.out
src/test/regress/sql/foreign_key.sql

index ed4ff0246cee1917d5eea84a32282428689f42a9..8b80fae5ebe782f124098f6d5ab9fafc41670304 100644 (file)
@@ -1310,10 +1310,9 @@ InitResultRelInfo(ResultRelInfo *resultRelInfo,
  *     Get a ResultRelInfo for a trigger target relation.
  *
  * Most of the time, triggers are fired on one of the result relations of the
- * query, and so we can just return a member of the es_result_relations array,
- * or the es_tuple_routing_result_relations list (if any). (Note: in self-join
- * situations there might be multiple members with the same OID; if so it
- * doesn't matter which one we pick.)
+ * query, and so we can just return a suitable one we already made and stored
+ * in the es_opened_result_relations or es_tuple_routing_result_relations
+ * Lists.
  *
  * However, it is sometimes necessary to fire triggers on other relations;
  * this happens mainly when an RI update trigger queues additional triggers
@@ -1333,11 +1332,20 @@ ExecGetTriggerResultRel(EState *estate, Oid relid,
    Relation    rel;
    MemoryContext oldcontext;
 
+   /*
+    * Before creating a new ResultRelInfo, check if we've already made and
+    * cached one for this relation.  We must ensure that the given
+    * 'rootRelInfo' matches the one stored in the cached ResultRelInfo as
+    * trigger handling for partitions can result in mixed requirements for
+    * what ri_RootResultRelInfo is set to.
+    */
+
    /* Search through the query result relations */
    foreach(l, estate->es_opened_result_relations)
    {
        rInfo = lfirst(l);
-       if (RelationGetRelid(rInfo->ri_RelationDesc) == relid)
+       if (RelationGetRelid(rInfo->ri_RelationDesc) == relid &&
+           rInfo->ri_RootResultRelInfo == rootRelInfo)
            return rInfo;
    }
 
@@ -1348,7 +1356,8 @@ ExecGetTriggerResultRel(EState *estate, Oid relid,
    foreach(l, estate->es_tuple_routing_result_relations)
    {
        rInfo = (ResultRelInfo *) lfirst(l);
-       if (RelationGetRelid(rInfo->ri_RelationDesc) == relid)
+       if (RelationGetRelid(rInfo->ri_RelationDesc) == relid &&
+           rInfo->ri_RootResultRelInfo == rootRelInfo)
            return rInfo;
    }
 
@@ -1356,7 +1365,8 @@ ExecGetTriggerResultRel(EState *estate, Oid relid,
    foreach(l, estate->es_trig_target_relations)
    {
        rInfo = (ResultRelInfo *) lfirst(l);
-       if (RelationGetRelid(rInfo->ri_RelationDesc) == relid)
+       if (RelationGetRelid(rInfo->ri_RelationDesc) == relid &&
+           rInfo->ri_RootResultRelInfo == rootRelInfo)
            return rInfo;
    }
    /* Nope, so we need a new one */
index 3a105f6306d5fd645bef869d872c47a69b4a7b83..c29b5ee8d79947aa2c623c4fbc13e3f6ea2cc59e 100644 (file)
@@ -3077,3 +3077,54 @@ SET client_min_messages TO warning;
 DROP SCHEMA fkpart12 CASCADE;
 RESET client_min_messages;
 RESET search_path;
+-- Exercise the column mapping code with foreign keys.  In this test we'll
+-- create a partitioned table which has a partition with a dropped column and
+-- check to ensure that an UPDATE cascades the changes correctly to the
+-- partitioned table.
+CREATE SCHEMA fkpart13;
+SET search_path TO fkpart13;
+CREATE TABLE fkpart13_t1 (a int PRIMARY KEY);
+CREATE TABLE fkpart13_t2 (
+  part_id int PRIMARY KEY,
+  column_to_drop int,
+  FOREIGN KEY (part_id) REFERENCES fkpart13_t1 ON UPDATE CASCADE ON DELETE CASCADE
+) PARTITION BY LIST (part_id);
+CREATE TABLE fkpart13_t2_p1 PARTITION OF fkpart13_t2 FOR VALUES IN (1);
+-- drop the column
+ALTER TABLE fkpart13_t2 DROP COLUMN column_to_drop;
+-- create a new partition without the dropped column
+CREATE TABLE fkpart13_t2_p2 PARTITION OF fkpart13_t2 FOR VALUES IN (2);
+CREATE TABLE fkpart13_t3 (
+  a int NOT NULL,
+  FOREIGN KEY (a)
+    REFERENCES fkpart13_t2
+    ON UPDATE CASCADE ON DELETE CASCADE
+);
+INSERT INTO fkpart13_t1 (a) VALUES (1);
+INSERT INTO fkpart13_t2 (part_id) VALUES (1);
+INSERT INTO fkpart13_t3 (a) VALUES (1);
+-- Test a cascading update works correctly with with the dropped column
+UPDATE fkpart13_t1 SET a = 2 WHERE a = 1;
+SELECT tableoid::regclass,* FROM fkpart13_t2;
+    tableoid    | part_id 
+----------------+---------
+ fkpart13_t2_p2 |       2
+(1 row)
+
+SELECT tableoid::regclass,* FROM fkpart13_t3;
+  tableoid   | a 
+-------------+---
+ fkpart13_t3 | 2
+(1 row)
+
+-- Exercise code in ExecGetTriggerResultRel() as there's been previous issues
+-- with ResultRelInfos being returned with the incorrect ri_RootResultRelInfo
+WITH cte AS (
+  UPDATE fkpart13_t2_p1 SET part_id = part_id
+) UPDATE fkpart13_t1 SET a = 2 WHERE a = 1;
+DROP SCHEMA fkpart13 CASCADE;
+NOTICE:  drop cascades to 3 other objects
+DETAIL:  drop cascades to table fkpart13_t1
+drop cascades to table fkpart13_t2
+drop cascades to table fkpart13_t3
+RESET search_path;
index 442a6cbc0939b4a7a935fe465efb83624a702650..980ad25b03e1df48374e27e9ab37809d9ec9c9e9 100644 (file)
@@ -2181,3 +2181,51 @@ SET client_min_messages TO warning;
 DROP SCHEMA fkpart12 CASCADE;
 RESET client_min_messages;
 RESET search_path;
+
+-- Exercise the column mapping code with foreign keys.  In this test we'll
+-- create a partitioned table which has a partition with a dropped column and
+-- check to ensure that an UPDATE cascades the changes correctly to the
+-- partitioned table.
+CREATE SCHEMA fkpart13;
+SET search_path TO fkpart13;
+
+CREATE TABLE fkpart13_t1 (a int PRIMARY KEY);
+
+CREATE TABLE fkpart13_t2 (
+  part_id int PRIMARY KEY,
+  column_to_drop int,
+  FOREIGN KEY (part_id) REFERENCES fkpart13_t1 ON UPDATE CASCADE ON DELETE CASCADE
+) PARTITION BY LIST (part_id);
+
+CREATE TABLE fkpart13_t2_p1 PARTITION OF fkpart13_t2 FOR VALUES IN (1);
+
+-- drop the column
+ALTER TABLE fkpart13_t2 DROP COLUMN column_to_drop;
+
+-- create a new partition without the dropped column
+CREATE TABLE fkpart13_t2_p2 PARTITION OF fkpart13_t2 FOR VALUES IN (2);
+
+CREATE TABLE fkpart13_t3 (
+  a int NOT NULL,
+  FOREIGN KEY (a)
+    REFERENCES fkpart13_t2
+    ON UPDATE CASCADE ON DELETE CASCADE
+);
+
+INSERT INTO fkpart13_t1 (a) VALUES (1);
+INSERT INTO fkpart13_t2 (part_id) VALUES (1);
+INSERT INTO fkpart13_t3 (a) VALUES (1);
+
+-- Test a cascading update works correctly with with the dropped column
+UPDATE fkpart13_t1 SET a = 2 WHERE a = 1;
+SELECT tableoid::regclass,* FROM fkpart13_t2;
+SELECT tableoid::regclass,* FROM fkpart13_t3;
+
+-- Exercise code in ExecGetTriggerResultRel() as there's been previous issues
+-- with ResultRelInfos being returned with the incorrect ri_RootResultRelInfo
+WITH cte AS (
+  UPDATE fkpart13_t2_p1 SET part_id = part_id
+) UPDATE fkpart13_t1 SET a = 2 WHERE a = 1;
+
+DROP SCHEMA fkpart13 CASCADE;
+RESET search_path;