postgres.git / summary / log / commit / refs
commit fab386f74888cb3cf4df9a20bcd24df2eb232bd2
Author: Amit Langote <amitlan@postgresql.org>
Date: Fri Jan 23 01:21:08 2026 +0000
First released in: 14.21, 15.16, 16.12, 17.8, 18.2
Fix bogus ctid requirement for dummy-root partitioned targets
ExecInitModifyTable() unconditionally required a ctid junk column even
when the target was a partitioned table. This led to spurious "could
not find junk ctid column" errors when all children were excluded and
only the dummy root result relation remained.
A partitioned table only appears in the result relations list when all
leaf partitions have been pruned, leaving the dummy root as the sole
entry. Assert this invariant (nrels == 1) and skip the ctid requirement.
Also adjust ExecModifyTable() to tolerate invalid ri_RowIdAttNo for
partitioned tables, which is safe since no rows will be processed in
this case.
Bug: #19099
Reported-by: Alexander Lakhin <exclusion@gmail.com>
Author: Amit Langote <amitlangote09@gmail.com>
Reviewed-by: Tender Wang <tndrwang@gmail.com>
Reviewed-by: Kirill Reshke <reshkekirill@gmail.com>
Reviewed-by: Tom Lane <tgl@sss.pgh.pa.us>
Discussion: https://postgr.es/m/19099-e05dcfa022fe553d%40postgresql.org
Backpatch-through: 14
contrib/file_fdw/expected/file_fdw.out | 15 +++++++++++++++
contrib/file_fdw/sql/file_fdw.sql | 18 ++++++++++++++++++
src/backend/executor/nodeModifyTable.c | 19 ++++++++++++++++---
3 files changed, 49 insertions(+), 3 deletions(-)
diff --git a/contrib/file_fdw/expected/file_fdw.out b/contrib/file_fdw/expected/file_fdw.out
index 6ae956f8801..be3658a7d14 100644
--- a/contrib/file_fdw/expected/file_fdw.out
+++ b/contrib/file_fdw/expected/file_fdw.out
@@ -415,6 +415,21 @@ SELECT tableoid::regclass, * FROM p2;
p2 | 2 | xyzzy
(3 rows)
+-- Test DELETE/UPDATE/MERGE on a partitioned table when all partitions
+-- are excluded and only the dummy root result relation remains. The
+-- operation is a no-op but should not fail regardless of whether the
+-- foreign child was processed (pruning off) or not (pruning on).
+DROP TABLE p2;
+SET enable_partition_pruning TO off;
+DELETE FROM pt WHERE false;
+UPDATE pt SET b = 'x' WHERE false;
+MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b)
+ ON false WHEN MATCHED THEN UPDATE SET b = s.b;
+SET enable_partition_pruning TO on;
+DELETE FROM pt WHERE false;
+UPDATE pt SET b = 'x' WHERE false;
+MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b)
+ ON false WHEN MATCHED THEN UPDATE SET b = s.b;
DROP TABLE pt;
-- generated column tests
\set filename :abs_srcdir '/data/list1.csv'
diff --git a/contrib/file_fdw/sql/file_fdw.sql b/contrib/file_fdw/sql/file_fdw.sql
index e91b9799c42..c5567ea82ea 100644
--- a/contrib/file_fdw/sql/file_fdw.sql
+++ b/contrib/file_fdw/sql/file_fdw.sql
@@ -226,6 +226,24 @@ UPDATE pt set a = 1 where a = 2; -- ERROR
SELECT tableoid::regclass, * FROM pt;
SELECT tableoid::regclass, * FROM p1;
SELECT tableoid::regclass, * FROM p2;
+
+-- Test DELETE/UPDATE/MERGE on a partitioned table when all partitions
+-- are excluded and only the dummy root result relation remains. The
+-- operation is a no-op but should not fail regardless of whether the
+-- foreign child was processed (pruning off) or not (pruning on).
+DROP TABLE p2;
+SET enable_partition_pruning TO off;
+DELETE FROM pt WHERE false;
+UPDATE pt SET b = 'x' WHERE false;
+MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b)
+ ON false WHEN MATCHED THEN UPDATE SET b = s.b;
+
+SET enable_partition_pruning TO on;
+DELETE FROM pt WHERE false;
+UPDATE pt SET b = 'x' WHERE false;
+MERGE INTO pt t USING (VALUES (1, 'x'::text)) AS s(a, b)
+ ON false WHEN MATCHED THEN UPDATE SET b = s.b;
+
DROP TABLE pt;
-- generated column tests
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index a846b064c9c..0d8a66aa9f0 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -3916,8 +3916,12 @@ ExecModifyTable(PlanState *pstate)
relkind == RELKIND_MATVIEW ||
relkind == RELKIND_PARTITIONED_TABLE)
{
- /* ri_RowIdAttNo refers to a ctid attribute */
- Assert(AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo));
+ /*
+ * ri_RowIdAttNo refers to a ctid attribute. See the comment
+ * in ExecInitModifyTable().
+ */
+ Assert(AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo) ||
+ relkind == RELKIND_PARTITIONED_TABLE);
datum = ExecGetJunkAttribute(slot,
resultRelInfo->ri_RowIdAttNo,
&isNull);
@@ -4307,7 +4311,16 @@ ExecInitModifyTable(ModifyTable *node, EState *estate, int eflags)
{
resultRelInfo->ri_RowIdAttNo =
ExecFindJunkAttributeInTlist(subplan->targetlist, "ctid");
- if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
+
+ /*
+ * For heap relations, a ctid junk attribute must be present.
+ * Partitioned tables should only appear here when all leaf
+ * partitions were pruned, in which case no rows can be
+ * produced and ctid is not needed.
+ */
+ if (relkind == RELKIND_PARTITIONED_TABLE)
+ Assert(nrels == 1);
+ else if (!AttributeNumberIsValid(resultRelInfo->ri_RowIdAttNo))
elog(ERROR, "could not find junk ctid column");
}
else if (relkind == RELKIND_FOREIGN_TABLE)
[parent: 3f56de3aad1b]