From e639fc0d40572b5c72afe817dbe66e37852a66b5 Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Thu, 7 May 2026 13:32:05 +0800 Subject: [PATCH v10 2/2] Fix FOR PORTION OF on inherited children with differing attnos --- src/backend/executor/nodeModifyTable.c | 36 ++++---- src/test/regress/expected/for_portion_of.out | 88 ++++++++++++++++++++ src/test/regress/sql/for_portion_of.sql | 48 +++++++++++ 3 files changed, 157 insertions(+), 15 deletions(-) diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c index 81f5afc9fb7..f6bf4ebbb11 100644 --- a/src/backend/executor/nodeModifyTable.c +++ b/src/backend/executor/nodeModifyTable.c @@ -1493,9 +1493,9 @@ ExecForPortionOfLeftovers(ModifyTableContext *context, fcinfo->args[1].isnull = false; /* - * For partitioned tables, we must read leftovers with the tuple descriptor - * of the child table, but insert into the root table to enable tuple - * routing. So leftoverSlot is configured with the root's tuple + * For partitioned tables, we must read leftovers with the tuple + * descriptor of the child table, but insert into the root table to enable + * tuple routing. So leftoverSlot is configured with the root's tuple * descriptor. However, for traditional table inheritance, we don't need * tuple routing and just insert directly into the child table to preserve * child-specific columns. In that case, leftoverSlot uses the child's @@ -5861,6 +5861,7 @@ ExecInitForPortionOf(ModifyTableState *mtstate, EState *estate, ResultRelInfo *r ForPortionOfState *leafState; ResultRelInfo *rootRelInfo = mtstate->rootResultRelInfo; ForPortionOfState *fpoState; + TupleConversionMap *map; if (!rootRelInfo) elog(ERROR, "no root relation but ri_forPortionOf is uninitialized"); @@ -5875,34 +5876,39 @@ ExecInitForPortionOf(ModifyTableState *mtstate, EState *estate, ResultRelInfo *r leafState->fp_rangeName = fpoState->fp_rangeName; leafState->fp_rangeType = fpoState->fp_rangeType; leafState->fp_targetRange = fpoState->fp_targetRange; + map = ExecGetChildToRootMap(resultRelInfo); + + /* + * fp_rangeAttno must match the tuple layout used for reading the old + * range value. The query uses the target relation's attno, so translate + * it to the child attno when the child has a different column layout. + */ + if (map) + leafState->fp_rangeAttno = map->attrMap->attnums[fpoState->fp_rangeAttno - 1]; + else + leafState->fp_rangeAttno = fpoState->fp_rangeAttno; /* - * For partitioned tables we must read the leftovers using the child table's - * tuple descriptor, but then insert them into the root table (using its - * tuple descriptor) so we get tuple routing. + * For partitioned tables we must read the leftovers using the child + * table's tuple descriptor, but then insert them into the root table + * (using its tuple descriptor) so we get tuple routing. * - * For traditional table inheritance, we read and insert directly into this - * resultRelInfo; no tuple routing to the parent is required. + * For traditional table inheritance, we read and insert directly into + * this resultRelInfo; no tuple routing to the parent is required. */ if (rootRelInfo->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) { - TupleConversionMap *map = ExecGetChildToRootMap(resultRelInfo); - if (map) - leafState->fp_rangeAttno = map->attrMap->attnums[fpoState->fp_rangeAttno - 1]; - else - leafState->fp_rangeAttno = fpoState->fp_rangeAttno; leafState->fp_Leftover = fpoState->fp_Leftover; } else { - leafState->fp_rangeAttno = fpoState->fp_rangeAttno; leafState->fp_Leftover = ExecInitExtraTupleSlot(mtstate->ps.state, RelationGetDescr(resultRelInfo->ri_RelationDesc), &TTSOpsVirtual); } - /* Each partition needs a slot matching its tuple descriptor */ + /* Each child relation needs a slot matching its tuple descriptor */ leafState->fp_Existing = table_slot_create(resultRelInfo->ri_RelationDesc, &mtstate->ps.state->es_tupleTable); diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out index 91241463991..8c0a35f85e5 100644 --- a/src/test/regress/expected/for_portion_of.out +++ b/src/test/regress/expected/for_portion_of.out @@ -2279,6 +2279,94 @@ SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; ----+----------+------ (0 rows) +TRUNCATE fpo_inh_child, fpo_inh_parent; +INSERT INTO fpo_inh_child (id, valid_at, name, description) VALUES + ('[1,2)', '[2018-01-01,2019-01-01)', 'one', 'initial'); +DELETE FROM fpo_inh_parent FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-10-01'; +SELECT tableoid::regclass, * FROM fpo_inh_parent ORDER BY valid_at; + tableoid | id | valid_at | name +---------------+-------+-------------------------+------ + fpo_inh_child | [1,2) | [2018-01-01,2018-04-01) | one + fpo_inh_child | [1,2) | [2018-10-01,2019-01-01) | one +(2 rows) + +SELECT * FROM fpo_inh_child ORDER BY valid_at; + id | valid_at | name | description +-------+-------------------------+------+------------- + [1,2) | [2018-01-01,2018-04-01) | one | initial + [1,2) | [2018-10-01,2019-01-01) | one | initial +(2 rows) + +SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; + id | valid_at | name +----+----------+------ +(0 rows) + DROP TABLE fpo_inh_parent CASCADE; NOTICE: drop cascades to table fpo_inh_child +-- UPDATE FOR PORTION OF with multiple inheritance +-- Leftover rows must stay in the child table, even if the range column's +-- attnum differs between the target parent and child. +CREATE TABLE temporal_parent ( + id int, + valid_at daterange, + name text +); +CREATE TABLE other_parent ( + prefix text, + note text +); +CREATE TABLE mi_child () INHERITS (other_parent, temporal_parent); +INSERT INTO mi_child (prefix, note, id, valid_at, name) VALUES + ('pfx', 'memo', 1, daterange('2000-01-01', '2010-01-01'), 'old'); +UPDATE temporal_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' + SET name = 'new' + WHERE id = 1; +SELECT tableoid::regclass, * FROM temporal_parent ORDER BY valid_at; + tableoid | id | valid_at | name +----------+----+-------------------------+------ + mi_child | 1 | [2000-01-01,2001-01-01) | old + mi_child | 1 | [2001-01-01,2002-01-01) | new + mi_child | 1 | [2002-01-01,2010-01-01) | old +(3 rows) + +SELECT * FROM mi_child ORDER BY valid_at; + prefix | note | id | valid_at | name +--------+------+----+-------------------------+------ + pfx | memo | 1 | [2000-01-01,2001-01-01) | old + pfx | memo | 1 | [2001-01-01,2002-01-01) | new + pfx | memo | 1 | [2002-01-01,2010-01-01) | old +(3 rows) + +SELECT * FROM ONLY temporal_parent ORDER BY valid_at; + id | valid_at | name +----+----------+------ +(0 rows) + +TRUNCATE mi_child, other_parent, temporal_parent; +INSERT INTO mi_child (prefix, note, id, valid_at, name) VALUES + ('pfx', 'memo', 1, daterange('2000-01-01', '2010-01-01'), 'old'); +DELETE FROM temporal_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' + WHERE id = 1; +SELECT tableoid::regclass, * FROM temporal_parent ORDER BY valid_at; + tableoid | id | valid_at | name +----------+----+-------------------------+------ + mi_child | 1 | [2000-01-01,2001-01-01) | old + mi_child | 1 | [2002-01-01,2010-01-01) | old +(2 rows) + +SELECT * FROM mi_child ORDER BY valid_at; + prefix | note | id | valid_at | name +--------+------+----+-------------------------+------ + pfx | memo | 1 | [2000-01-01,2001-01-01) | old + pfx | memo | 1 | [2002-01-01,2010-01-01) | old +(2 rows) + +SELECT * FROM ONLY temporal_parent ORDER BY valid_at; + id | valid_at | name +----+----------+------ +(0 rows) + +DROP TABLE temporal_parent CASCADE; +NOTICE: drop cascades to table mi_child RESET datestyle; diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql index 04e0dba6375..b1144a13782 100644 --- a/src/test/regress/sql/for_portion_of.sql +++ b/src/test/regress/sql/for_portion_of.sql @@ -1484,6 +1484,54 @@ SELECT * FROM fpo_inh_child ORDER BY valid_at; -- No rows should have leaked into the parent. SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; +TRUNCATE fpo_inh_child, fpo_inh_parent; +INSERT INTO fpo_inh_child (id, valid_at, name, description) VALUES + ('[1,2)', '[2018-01-01,2019-01-01)', 'one', 'initial'); + +DELETE FROM fpo_inh_parent FOR PORTION OF valid_at FROM '2018-04-01' TO '2018-10-01'; + +SELECT tableoid::regclass, * FROM fpo_inh_parent ORDER BY valid_at; +SELECT * FROM fpo_inh_child ORDER BY valid_at; +SELECT * FROM ONLY fpo_inh_parent ORDER BY valid_at; + DROP TABLE fpo_inh_parent CASCADE; +-- UPDATE FOR PORTION OF with multiple inheritance +-- Leftover rows must stay in the child table, even if the range column's +-- attnum differs between the target parent and child. +CREATE TABLE temporal_parent ( + id int, + valid_at daterange, + name text +); +CREATE TABLE other_parent ( + prefix text, + note text +); +CREATE TABLE mi_child () INHERITS (other_parent, temporal_parent); + +INSERT INTO mi_child (prefix, note, id, valid_at, name) VALUES + ('pfx', 'memo', 1, daterange('2000-01-01', '2010-01-01'), 'old'); + +UPDATE temporal_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' + SET name = 'new' + WHERE id = 1; + +SELECT tableoid::regclass, * FROM temporal_parent ORDER BY valid_at; +SELECT * FROM mi_child ORDER BY valid_at; +SELECT * FROM ONLY temporal_parent ORDER BY valid_at; + +TRUNCATE mi_child, other_parent, temporal_parent; +INSERT INTO mi_child (prefix, note, id, valid_at, name) VALUES + ('pfx', 'memo', 1, daterange('2000-01-01', '2010-01-01'), 'old'); + +DELETE FROM temporal_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01' + WHERE id = 1; + +SELECT tableoid::regclass, * FROM temporal_parent ORDER BY valid_at; +SELECT * FROM mi_child ORDER BY valid_at; +SELECT * FROM ONLY temporal_parent ORDER BY valid_at; + +DROP TABLE temporal_parent CASCADE; + RESET datestyle; -- 2.50.1 (Apple Git-155)