v1-0001-Fix-FOR-PORTION-OF-leftovers-for-inheritance-chil.patch
application/octet-stream
Filename: v1-0001-Fix-FOR-PORTION-OF-leftovers-for-inheritance-chil.patch
Type: application/octet-stream
Part: 0
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch v1-0001
Subject: Fix FOR PORTION OF leftovers for inheritance children
| File | + | − |
|---|---|---|
| src/backend/executor/nodeModifyTable.c | 53 | 23 |
| src/test/regress/expected/for_portion_of.out | 119 | 0 |
| src/test/regress/sql/for_portion_of.sql | 80 | 0 |
From 92abeba40fb1ef795a7f0eea198d7c144aef20fe Mon Sep 17 00:00:00 2001
From: "Chao Li (Evan)" <lic@highgo.com>
Date: Thu, 7 May 2026 11:23:10 +0800
Subject: [PATCH v1] Fix FOR PORTION OF leftovers for inheritance children
ExecForPortionOfLeftovers() assumed that any result relation with
ri_RootResultRelInfo should reinsert temporal leftovers through the root
relation. That is correct for partitioned tables, where tuple routing is
needed, but it is wrong for plain inheritance.
When UPDATE/DELETE FOR PORTION OF is run on an inheritance parent and a
child row is split, the leftover rows must be inserted back into the child
relation. Reinserting through the parent can lose child-only columns and
place the leftover rows in the wrong relation.
Fix this by distinguishing partitioned-table routing from plain
inheritance. For partitioned tables, keep using the root leftover slot and
insert through the root relation. For plain inheritance children, use a
leftover slot matching the child relation and insert directly into the
child. Also keep translating the application-time column attno for child
relations, so multiple-inheritance cases with different attribute numbers
are handled correctly.
Add regression tests for UPDATE and DELETE FOR PORTION OF on inheritance
children, including a multiple-inheritance case where the range column has
a different attnum in the parent and child.
Author: Chao Li <lic@highgo.com>
Reviewed-by:
Discussion: https://postgr.es/m/
---
src/backend/executor/nodeModifyTable.c | 76 ++++++++----
src/test/regress/expected/for_portion_of.out | 119 +++++++++++++++++++
src/test/regress/sql/for_portion_of.sql | 80 +++++++++++++
3 files changed, 252 insertions(+), 23 deletions(-)
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index 4cb057ca4f9..df3e3bb1b98 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -1409,7 +1409,11 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
ModifyTableState *mtstate = context->mtstate;
ModifyTable *node = (ModifyTable *) mtstate->ps.plan;
ForPortionOfExpr *forPortionOf = (ForPortionOfExpr *) node->forPortionOf;
- AttrNumber rangeAttno;
+ AttrNumber actualRangeAttno; /* attno of range column in the current
+ * result relation */
+ AttrNumber targetRangeAttno; /* attno of range column in the target
+ * table of the query */
+ AttrNumber leftoverRangeAttno;
Datum oldRange;
TypeCacheEntry *typcache;
ForPortionOfState *fpoState;
@@ -1422,18 +1426,30 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
FmgrInfo flinfo;
PgStat_FunctionCallUsage fcusage;
ReturnSetInfo rsi;
+ ResultRelInfo *rootRri = resultRelInfo->ri_RootResultRelInfo;
bool didInit = false;
bool shouldFree = false;
+ bool partitionRouting;
LOCAL_FCINFO(fcinfo, 2);
+ partitionRouting =
+ rootRri != NULL &&
+ rootRri->ri_RelationDesc->rd_rel->relkind == RELKIND_PARTITIONED_TABLE;
+ targetRangeAttno = forPortionOf->rangeVar->varattno;
+
if (!resultRelInfo->ri_forPortionOf)
{
/*
- * If we don't have a ForPortionOfState yet, we must be a partition
- * child being hit for the first time. Make a copy from the root, with
- * our own TupleTableSlot. We do this lazily so that we don't pay the
- * price of unused partitions.
+ * If we don't have a ForPortionOfState yet, we must be a child
+ * relation being hit for the first time. Make a copy from the root,
+ * with slots matching the child's tuple descriptor.
+ *
+ * Partitions share the root leftover slot, since they must insert via
+ * the root relation to get tuple routing. Plain inheritance children
+ * must keep their own leftover slot and insert back into the child,
+ * or else child-only column values and physical placement would be
+ * lost.
*/
ForPortionOfState *leafState = makeNode(ForPortionOfState);
@@ -1447,8 +1463,15 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
leafState->fp_rangeType = fpoState->fp_rangeType;
leafState->fp_rangeAttno = fpoState->fp_rangeAttno;
leafState->fp_targetRange = fpoState->fp_targetRange;
- leafState->fp_Leftover = fpoState->fp_Leftover;
- /* Each partition needs a slot matching its tuple descriptor */
+ if (partitionRouting)
+ leafState->fp_Leftover = fpoState->fp_Leftover;
+ else
+ leafState->fp_Leftover =
+ ExecInitExtraTupleSlot(mtstate->ps.state,
+ resultRelInfo->ri_RelationDesc->rd_att,
+ &TTSOpsVirtual);
+
+ /* Each child relation needs a slot matching its tuple descriptor */
leafState->fp_Existing =
table_slot_create(resultRelInfo->ri_RelationDesc,
&mtstate->ps.state->es_tupleTable);
@@ -1476,20 +1499,22 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
elog(ERROR, "failed to fetch tuple for FOR PORTION OF");
/*
- * Get the old range of the record being updated/deleted. Must read with
- * the attno of the leaf partition being updated.
+ * Get the old range of the record being updated/deleted. The query
+ * expression uses the target relation's attno, but the tuple slot may
+ * belong to a child relation. If so, translate it to the corresponding
+ * child attno.
*/
- rangeAttno = forPortionOf->rangeVar->varattno;
- if (resultRelInfo->ri_RootResultRelInfo)
+ actualRangeAttno = targetRangeAttno;
+ if (rootRri != NULL)
map = ExecGetChildToRootMap(resultRelInfo);
if (map != NULL)
- rangeAttno = map->attrMap->attnums[rangeAttno - 1];
+ actualRangeAttno = map->attrMap->attnums[actualRangeAttno - 1];
slot_getallattrs(oldtupleSlot);
- if (oldtupleSlot->tts_isnull[rangeAttno - 1])
+ if (oldtupleSlot->tts_isnull[actualRangeAttno - 1])
elog(ERROR, "found a NULL range in a temporal table");
- oldRange = oldtupleSlot->tts_values[rangeAttno - 1];
+ oldRange = oldtupleSlot->tts_values[actualRangeAttno - 1];
/*
* Get the range's type cache entry. This is worth caching for the whole
@@ -1529,10 +1554,15 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
/*
* If there are partitions, we must insert into the root table, so we get
* tuple routing. We already set up leftoverSlot with the root tuple
- * descriptor.
+ * descriptor for that case.
*/
- if (resultRelInfo->ri_RootResultRelInfo)
- resultRelInfo = resultRelInfo->ri_RootResultRelInfo;
+ if (partitionRouting)
+ {
+ resultRelInfo = rootRri;
+ leftoverRangeAttno = targetRangeAttno;
+ }
+ else
+ leftoverRangeAttno = actualRangeAttno;
/*
* Insert a leftover for each value returned by the without_portion helper
@@ -1574,11 +1604,11 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
{
/*
* Make a copy of the pre-UPDATE row. Then we'll overwrite the
- * range column below. Convert oldtuple to the base table's format
- * if necessary. We need to insert temporal leftovers through the
- * root partition so they get routed correctly.
+ * range column below. Only partitioned targets need conversion to
+ * the root table's format, because they reinsert through the root
+ * relation for tuple routing.
*/
- if (map != NULL)
+ if (partitionRouting && map != NULL)
{
leftoverSlot = execute_attr_map_slot(map->attrMap,
oldtupleSlot,
@@ -1601,8 +1631,8 @@ ExecForPortionOfLeftovers(ModifyTableContext *context,
didInit = true;
}
- leftoverSlot->tts_values[forPortionOf->rangeVar->varattno - 1] = leftover;
- leftoverSlot->tts_isnull[forPortionOf->rangeVar->varattno - 1] = false;
+ leftoverSlot->tts_values[leftoverRangeAttno - 1] = leftover;
+ leftoverSlot->tts_isnull[leftoverRangeAttno - 1] = false;
ExecMaterializeSlot(leftoverSlot);
/*
diff --git a/src/test/regress/expected/for_portion_of.out b/src/test/regress/expected/for_portion_of.out
index 0c0a205c44b..7d7847c9ad7 100644
--- a/src/test/regress/expected/for_portion_of.out
+++ b/src/test/regress/expected/for_portion_of.out
@@ -2097,6 +2097,125 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
(4 rows)
DROP TABLE temporal_partitioned;
+-- UPDATE/DELETE FOR PORTION OF with inheritance
+CREATE TABLE fpo_parent (
+ id int,
+ valid_at daterange,
+ name text
+);
+CREATE TABLE fpo_child (
+ extra text
+) INHERITS (fpo_parent);
+INSERT INTO fpo_child VALUES
+ (1, daterange('2000-01-01', '2010-01-01'), 'old', 'x');
+UPDATE fpo_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01'
+ SET name = 'new'
+ WHERE id = 1;
+SELECT * FROM ONLY fpo_parent;
+ id | valid_at | name
+----+----------+------
+(0 rows)
+
+SELECT * FROM ONLY fpo_child;
+ id | valid_at | name | extra
+----+-------------------------+------+-------
+ 1 | [2001-01-01,2002-01-01) | new | x
+ 1 | [2000-01-01,2001-01-01) | old | x
+ 1 | [2002-01-01,2010-01-01) | old | x
+(3 rows)
+
+TRUNCATE TABLE fpo_child, fpo_parent;
+INSERT INTO fpo_child VALUES
+ (1, daterange('2000-01-01', '2010-01-01'), 'old', 'x');
+DELETE FROM fpo_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01'
+ WHERE id = 1;
+SELECT * FROM ONLY fpo_parent;
+ id | valid_at | name
+----+----------+------
+(0 rows)
+
+SELECT * FROM ONLY fpo_child;
+ id | valid_at | name | extra
+----+-------------------------+------+-------
+ 1 | [2000-01-01,2001-01-01) | old | x
+ 1 | [2002-01-01,2010-01-01) | old | x
+(2 rows)
+
+DROP TABLE fpo_child, fpo_parent;
+-- UPDATE/DELETE FOR PORTION OF with inheritance and differing attnos
+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);
+-- attnum of the range column is different in temporal_parent and mi_child
+SELECT attnum, attname
+ FROM pg_attribute
+ WHERE attrelid = 'temporal_parent'::regclass
+ AND attnum > 0 AND NOT attisdropped
+ ORDER BY attnum;
+ attnum | attname
+--------+----------
+ 1 | id
+ 2 | valid_at
+ 3 | name
+(3 rows)
+
+SELECT attnum, attname
+ FROM pg_attribute
+ WHERE attrelid = 'mi_child'::regclass
+ AND attnum > 0 AND NOT attisdropped
+ ORDER BY attnum;
+ attnum | attname
+--------+----------
+ 1 | prefix
+ 2 | note
+ 3 | id
+ 4 | valid_at
+ 5 | name
+(5 rows)
+
+INSERT INTO mi_child 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 * FROM ONLY temporal_parent;
+ id | valid_at | name
+----+----------+------
+(0 rows)
+
+SELECT * FROM ONLY mi_child;
+ prefix | note | id | valid_at | name
+--------+------+----+-------------------------+------
+ pfx | memo | 1 | [2001-01-01,2002-01-01) | new
+ pfx | memo | 1 | [2000-01-01,2001-01-01) | old
+ pfx | memo | 1 | [2002-01-01,2010-01-01) | old
+(3 rows)
+
+TRUNCATE TABLE mi_child, other_parent, temporal_parent;
+INSERT INTO mi_child 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 * FROM ONLY temporal_parent;
+ id | valid_at | name
+----+----------+------
+(0 rows)
+
+SELECT * FROM ONLY mi_child;
+ 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)
+
+DROP TABLE mi_child, other_parent, temporal_parent;
-- UPDATE/DELETE FOR PORTION OF with RULEs
CREATE TABLE fpo_rule (f1 bigint, f2 int4range);
INSERT INTO fpo_rule VALUES (1, '[1, 11)');
diff --git a/src/test/regress/sql/for_portion_of.sql b/src/test/regress/sql/for_portion_of.sql
index fd79a9b78e7..83a2bc9a82c 100644
--- a/src/test/regress/sql/for_portion_of.sql
+++ b/src/test/regress/sql/for_portion_of.sql
@@ -1365,6 +1365,86 @@ SELECT * FROM temporal_partitioned_5 ORDER BY id, valid_at;
DROP TABLE temporal_partitioned;
+-- UPDATE/DELETE FOR PORTION OF with inheritance
+CREATE TABLE fpo_parent (
+ id int,
+ valid_at daterange,
+ name text
+);
+CREATE TABLE fpo_child (
+ extra text
+) INHERITS (fpo_parent);
+
+INSERT INTO fpo_child VALUES
+ (1, daterange('2000-01-01', '2010-01-01'), 'old', 'x');
+
+UPDATE fpo_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01'
+ SET name = 'new'
+ WHERE id = 1;
+
+SELECT * FROM ONLY fpo_parent;
+SELECT * FROM ONLY fpo_child;
+
+TRUNCATE TABLE fpo_child, fpo_parent;
+
+INSERT INTO fpo_child VALUES
+ (1, daterange('2000-01-01', '2010-01-01'), 'old', 'x');
+
+DELETE FROM fpo_parent FOR PORTION OF valid_at FROM '2001-01-01' TO '2002-01-01'
+ WHERE id = 1;
+
+SELECT * FROM ONLY fpo_parent;
+SELECT * FROM ONLY fpo_child;
+
+DROP TABLE fpo_child, fpo_parent;
+
+-- UPDATE/DELETE FOR PORTION OF with inheritance and differing attnos
+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);
+
+-- attnum of the range column is different in temporal_parent and mi_child
+SELECT attnum, attname
+ FROM pg_attribute
+ WHERE attrelid = 'temporal_parent'::regclass
+ AND attnum > 0 AND NOT attisdropped
+ ORDER BY attnum;
+SELECT attnum, attname
+ FROM pg_attribute
+ WHERE attrelid = 'mi_child'::regclass
+ AND attnum > 0 AND NOT attisdropped
+ ORDER BY attnum;
+
+INSERT INTO mi_child 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 * FROM ONLY temporal_parent;
+SELECT * FROM ONLY mi_child;
+
+TRUNCATE TABLE mi_child, other_parent, temporal_parent;
+
+INSERT INTO mi_child 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 * FROM ONLY temporal_parent;
+SELECT * FROM ONLY mi_child;
+
+DROP TABLE mi_child, other_parent, temporal_parent;
+
-- UPDATE/DELETE FOR PORTION OF with RULEs
CREATE TABLE fpo_rule (f1 bigint, f2 int4range);
INSERT INTO fpo_rule VALUES (1, '[1, 11)');
--
2.50.1 (Apple Git-155)