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
* 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
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;
}
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;
}
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 */
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;
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;