v3-0002-core-remote-tableoid-param.patch
application/octet-stream
Filename: v3-0002-core-remote-tableoid-param.patch
Type: application/octet-stream
Part: 1
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 v3-0002
Subject: DELETE/UPDATE more than one row in partitioned foreign table (2/3)
| File | + | − |
|---|---|---|
| src/backend/executor/execMain.c | 4 | 0 |
| src/backend/executor/execTuples.c | 2 | 2 |
| src/backend/executor/nodeForeignscan.c | 2 | 0 |
| src/backend/optimizer/path/allpaths.c | 20 | 0 |
| src/backend/optimizer/plan/createplan.c | 19 | 0 |
| src/backend/optimizer/plan/initsplan.c | 45 | 0 |
| src/backend/optimizer/plan/planner.c | 9 | 5 |
| src/backend/optimizer/plan/setrefs.c | 56 | 0 |
| src/backend/optimizer/plan/subselect.c | 6 | 1 |
| src/backend/optimizer/util/appendinfo.c | 27 | 0 |
| src/backend/optimizer/util/inherit.c | 1 | 0 |
| src/backend/optimizer/util/relnode.c | 21 | 0 |
| src/backend/utils/adt/ruleutils.c | 1 | 1 |
| src/include/executor/tuptable.h | 1 | 0 |
| src/include/nodes/pathnodes.h | 7 | 0 |
| src/include/nodes/primnodes.h | 1 | 0 |
From 5b550edb91978a5250eaff41a9e5c72b8a111d0d Mon Sep 17 00:00:00 2001
From: Nikita Malakhov <n.malakhov@postgrespro.ru>
Date: Thu, 14 May 2026 20:18:34 +0300
Subject: [PATCH] DELETE/UPDATE more than one row in partitioned foreign table
(2/3)
There is known bug in delete and update rows in partitioned foreign
tables via FDW [1]
Core patch - introduction of remote table OID returned from remote
query and passing it through planner and executor to use in foreign
queries.
[1] Discussion: https://www.postgresql.org/message-id/flat/20250718175314.4513c00a%40karst
---
src/backend/executor/execMain.c | 4 ++
src/backend/executor/execTuples.c | 4 +-
src/backend/executor/nodeForeignscan.c | 2 +
src/backend/optimizer/path/allpaths.c | 20 +++++++++
src/backend/optimizer/plan/createplan.c | 19 +++++++++
src/backend/optimizer/plan/initsplan.c | 45 ++++++++++++++++++++
src/backend/optimizer/plan/planner.c | 14 ++++---
src/backend/optimizer/plan/setrefs.c | 56 +++++++++++++++++++++++++
src/backend/optimizer/plan/subselect.c | 7 +++-
src/backend/optimizer/util/appendinfo.c | 27 ++++++++++++
src/backend/optimizer/util/inherit.c | 1 +
src/backend/optimizer/util/relnode.c | 21 ++++++++++
src/backend/utils/adt/ruleutils.c | 2 +-
src/include/executor/tuptable.h | 1 +
src/include/nodes/pathnodes.h | 7 ++++
src/include/nodes/primnodes.h | 1 +
16 files changed, 222 insertions(+), 9 deletions(-)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 4b30f768680..b7b3b73941e 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -2621,6 +2621,10 @@ ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist)
resname);
if (!AttributeNumberIsValid(aerm->ctidAttNo))
elog(ERROR, "could not find junk %s column", resname);
+
+ snprintf(resname, sizeof(resname), "tableoid%u", erm->rowmarkId);
+ aerm->toidAttNo = ExecFindJunkAttributeInTlist(targetlist,
+ resname);
}
else
{
diff --git a/src/backend/executor/execTuples.c b/src/backend/executor/execTuples.c
index 6322a8a64b3..f8c66bdaef6 100644
--- a/src/backend/executor/execTuples.c
+++ b/src/backend/executor/execTuples.c
@@ -1911,8 +1911,7 @@ ExecStoreAllNullTuple(TupleTableSlot *slot)
* Until the slot is materialized, the contents of the slot depend on the
* datum.
*/
-void
-ExecStoreHeapTupleDatum(Datum data, TupleTableSlot *slot)
+void ExecStoreHeapTupleDatum(Datum data, TupleTableSlot *slot)
{
HeapTupleData tuple = {0};
HeapTupleHeader td;
@@ -1922,6 +1921,7 @@ ExecStoreHeapTupleDatum(Datum data, TupleTableSlot *slot)
tuple.t_len = HeapTupleHeaderGetDatumLength(td);
tuple.t_self = td->t_ctid;
tuple.t_data = td;
+ tuple.t_tableOid = InvalidOid;
ExecClearTuple(slot);
diff --git a/src/backend/executor/nodeForeignscan.c b/src/backend/executor/nodeForeignscan.c
index 6f0daddce07..21b3e9a961b 100644
--- a/src/backend/executor/nodeForeignscan.c
+++ b/src/backend/executor/nodeForeignscan.c
@@ -65,6 +65,8 @@ ForeignNext(ForeignScanState *node)
* Insert valid value into tableoid, the only actually-useful system
* column.
*/
+
+ slot->tts_remoteOid = slot->tts_tableOid;
if (plan->fsSystemCol && !TupIsNull(slot))
slot->tts_tableOid = RelationGetRelid(node->ss.ss_currentRelation);
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 61093f222a1..040763caf35 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -1161,6 +1161,26 @@ set_append_rel_size(PlannerInfo *root, RelOptInfo *rel,
(Node *) rel->reltarget->exprs,
1, &appinfo);
+ /* Do it if fdw is partition */
+ if (planner_rt_fetch(childRTindex, root)->relkind == RELKIND_FOREIGN_TABLE &&
+ !bms_is_empty(root->glob->foreignParamIDs))
+ {
+ foreach(lc, root->processed_tlist)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ Param *param = (Param *) tle->expr;
+
+ if (tle->resjunk && IsA(param, Param) &&
+ IS_FOREIGN_PARAM(root, param) &&
+ param->target_rte == childRTindex) // TODO same for another case
+ {
+ /* XXX is copyObject necessary here? */
+ childrel->reltarget->exprs =
+ lappend(childrel->reltarget->exprs, copyObject(param));
+ }
+ }
+ }
+
/*
* We have to make child entries in the EquivalenceClass data
* structures as well. This is needed either if the parent
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index de6a183da79..00ad2dcb07e 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -980,6 +980,25 @@ use_physical_tlist(PlannerInfo *root, Path *path, int flags)
}
}
+ /*
+ * Also, can't do it to a ForeignPath if the path is requested to emit
+ * Params generated by the FDW.
+ */
+ if (IsA(path, ForeignPath) &&
+ path->parent->relid == root->parse->resultRelation &&
+ !bms_is_empty(root->glob->foreignParamIDs))
+ {
+ foreach(lc, path->pathtarget->exprs)
+ {
+ Param *param = (Param *) lfirst(lc);
+ if (param && IsA(param, Param))
+ {
+ Assert(IS_FOREIGN_PARAM(root, param));
+ return false;
+ }
+ }
+ }
+
return true;
}
diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c
index b38422c47a4..cc09065e561 100644
--- a/src/backend/optimizer/plan/initsplan.c
+++ b/src/backend/optimizer/plan/initsplan.c
@@ -32,6 +32,7 @@
#include "optimizer/planner.h"
#include "optimizer/restrictinfo.h"
#include "parser/analyze.h"
+#include "parser/parsetree.h"
#include "rewrite/rewriteManip.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -243,6 +244,40 @@ add_other_rels_to_query(PlannerInfo *root)
*
*****************************************************************************/
+/*
+ * add_params_to_result_rel
+ * If the query's final tlist contains Params the FDW generated, add
+ * targetlist entries for each such Param to the result relation.
+ */
+static void
+add_params_to_result_rel(PlannerInfo *root, List *final_tlist)
+{
+ RelOptInfo *target_rel = find_base_rel(root, root->parse->resultRelation);
+ ListCell *lc;
+
+ /*
+ * If no parameters have been generated by any FDWs, we certainly don't
+ * need to do anything here.
+ */
+ if (bms_is_empty(root->glob->foreignParamIDs))
+ return;
+
+ foreach(lc, final_tlist)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ Param *param = (Param *) tle->expr;
+
+ if (tle->resjunk && IsA(param, Param) &&
+ IS_FOREIGN_PARAM(root, param) &&
+ param->target_rte == target_rel->relid)
+ {
+ /* XXX is copyObject necessary here? */
+ target_rel->reltarget->exprs = lappend(target_rel->reltarget->exprs,
+ copyObject(param));
+ }
+ }
+}
+
/*
* build_base_rel_tlists
* Add targetlist entries for each var needed in the query's final tlist
@@ -282,6 +317,16 @@ build_base_rel_tlists(PlannerInfo *root, List *final_tlist)
list_free(having_vars);
}
}
+
+ if (root->parse->commandType == CMD_UPDATE ||
+ root->parse->commandType == CMD_DELETE)
+ {
+ int result_relation = root->parse->resultRelation;
+
+ if (planner_rt_fetch(result_relation, root)->relkind == RELKIND_FOREIGN_TABLE)
+ add_params_to_result_rel(root, final_tlist);
+
+ }
}
/*
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index f4689e7c9f8..a560ecfbd8a 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -393,6 +393,7 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
glob->dependsOnRole = false;
glob->partition_directory = NULL;
glob->rel_notnullatts_hash = NULL;
+ glob->foreignParamIDs = NULL;
/*
* Assess whether it's feasible to use parallel mode for this query. We
@@ -621,12 +622,15 @@ standard_planner(Query *parse, const char *query_string, int cursorOptions,
}
/*
- * If any Params were generated, run through the plan tree and compute
- * each plan node's extParam/allParam sets. Ideally we'd merge this into
- * set_plan_references' tree traversal, but for now it has to be separate
- * because we need to visit subplans before not after main plan.
+ * If any Params were generated by the planner not by FDWs, run through
+ * the plan tree and compute each plan node's extParam/allParam sets.
+ * (Params added by FDWs are irrelevant for parameter change signaling.)
+ * Ideally we'd merge this into set_plan_references' tree traversal, but
+ * for now it has to be separate because we need to visit subplans before
+ * not after main plan.
*/
- if (glob->paramExecTypes != NIL)
+ if (glob->paramExecTypes != NIL &&
+ bms_num_members(glob->foreignParamIDs) < list_length(glob->paramExecTypes))
{
Assert(list_length(glob->subplans) == list_length(glob->subroots));
forboth(lp, glob->subplans, lr, glob->subroots)
diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c
index ff0e875f2a2..4fd162920db 100644
--- a/src/backend/optimizer/plan/setrefs.c
+++ b/src/backend/optimizer/plan/setrefs.c
@@ -3310,7 +3310,42 @@ fix_join_expr_mutator(Node *node, fix_join_expr_context *context)
}
/* Special cases (apply only AFTER failing to match to lower tlist) */
if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+
+ /*
+ * If the Param is a PARAM_EXEC Param generated by an FDW, it should
+ * have bubbled up from a lower plan node; convert it into a simple
+ * Var referencing the output of the subplan.
+ *
+ * Note: set_join_references() would have kept has_non_vars=true for
+ * the subplan emitting the Param since it effectively belong to the
+ * result relation and that relation can never be the nullable side of
+ * an outer join.
+ */
+ if (IS_FOREIGN_PARAM(context->root, param))
+ {
+ if (context->outer_itlist && context->outer_itlist->has_non_vars)
+ {
+ newvar = search_indexed_tlist_for_non_var((Expr *) node,
+ context->outer_itlist,
+ OUTER_VAR);
+ if (newvar)
+ return (Node *) newvar;
+ }
+ if (context->inner_itlist && context->inner_itlist->has_non_vars)
+ {
+ newvar = search_indexed_tlist_for_non_var((Expr *) node,
+ context->inner_itlist,
+ INNER_VAR);
+ if (newvar)
+ return (Node *) newvar;
+ }
+ // XXX Is it an error to be here?
+ }
+ /* If not, do fix_param_node() */
return fix_param_node(context->root, (Param *) node);
+ }
if (IsA(node, AlternativeSubPlan))
return fix_join_expr_mutator(fix_alternative_subplan(context->root,
(AlternativeSubPlan *) node,
@@ -3421,7 +3456,28 @@ fix_upper_expr_mutator(Node *node, fix_upper_expr_context *context)
}
/* Special cases (apply only AFTER failing to match to lower tlist) */
if (IsA(node, Param))
+ {
+ Param *param = (Param *) node;
+ /*
+ * If the Param is a PARAM_EXEC Param generated by an FDW, it should
+ * have bubbled up from a lower plan node; convert it into a simple
+ * Var referencing the output of the subplan.
+ */
+ if (IS_FOREIGN_PARAM(context->root, param))
+ {
+ if (context->subplan_itlist->has_non_vars)
+ {
+ newvar = search_indexed_tlist_for_non_var((Expr *) node,
+ context->subplan_itlist,
+ context->newvarno);
+ if (newvar)
+ return (Node *) newvar;
+ }
+ // XXX Is it an error to be here?
+ }
+ /* If not, do fix_param_node() */
return fix_param_node(context->root, (Param *) node);
+ }
if (IsA(node, Aggref))
{
Aggref *aggref = (Aggref *) node;
diff --git a/src/backend/optimizer/plan/subselect.c b/src/backend/optimizer/plan/subselect.c
index ccec1eaa7fe..375fdf27e53 100644
--- a/src/backend/optimizer/plan/subselect.c
+++ b/src/backend/optimizer/plan/subselect.c
@@ -3192,7 +3192,12 @@ finalize_primnode(Node *node, finalize_primnode_context *context)
{
int paramid = ((Param *) node)->paramid;
- context->paramids = bms_add_member(context->paramids, paramid);
+ /*
+ * Params added by FDWs are irrelevant for parameter change
+ * signaling.
+ */
+ if (!bms_is_member(paramid, context->root->glob->foreignParamIDs))
+ context->paramids = bms_add_member(context->paramids, paramid);
}
return false; /* no more to do here */
}
diff --git a/src/backend/optimizer/util/appendinfo.c b/src/backend/optimizer/util/appendinfo.c
index 778e4662f6e..7089a267125 100644
--- a/src/backend/optimizer/util/appendinfo.c
+++ b/src/backend/optimizer/util/appendinfo.c
@@ -948,6 +948,29 @@ add_row_identity_var(PlannerInfo *root, Var *orig_var,
root->processed_tlist = lappend(root->processed_tlist, tle);
}
+static void
+fix_foreign_params(PlannerInfo *root, List *tlist)
+{
+ ListCell *lc;
+
+ foreach(lc, tlist)
+ {
+ TargetEntry *tle = (TargetEntry *) lfirst(lc);
+ Param *param = (Param *) tle->expr;
+
+ if (tle->resjunk && IsA(param, Param) &&
+ param->paramkind == PARAM_EXEC &&
+ param->paramid == -1)
+ {
+ param->paramid = list_length(root->glob->paramExecTypes);
+ root->glob->paramExecTypes =
+ lappend_oid(root->glob->paramExecTypes, param->paramtype);
+ root->glob->foreignParamIDs =
+ bms_add_member(root->glob->foreignParamIDs, param->paramid);
+ }
+ }
+}
+
/*
* add_row_identity_columns
*
@@ -992,8 +1015,12 @@ add_row_identity_columns(PlannerInfo *root, Index rtindex,
fdwroutine = GetFdwRoutineForRelation(target_relation, false);
if (fdwroutine->AddForeignUpdateTargets != NULL)
+ {
+
fdwroutine->AddForeignUpdateTargets(root, rtindex,
target_rte, target_relation);
+ fix_foreign_params(root, root->processed_tlist);
+ }
/*
* For UPDATE, we need to make the FDW fetch unchanged columns by
diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c
index 6a7b9edff3f..d8f08485e39 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -294,6 +294,7 @@ expand_inherited_rtentry(PlannerInfo *root, RelOptInfo *rel,
InvalidOid,
0);
snprintf(resname, sizeof(resname), "tableoid%u", oldrc->rowmarkId);
+
tle = makeTargetEntry((Expr *) var,
list_length(root->processed_tlist) + 1,
pstrdup(resname),
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index 3fc2c2f71d0..655b2b00bd7 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -1320,6 +1320,27 @@ build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel,
}
continue;
}
+ /*
+ * We allow FDWs to have PARAM_EXEC Params here.
+ */
+ else if (IsA(var, Param))
+ {
+ Param *param = (Param *) var;
+
+ Assert(IS_FOREIGN_PARAM(root, param));
+
+ joinrel->reltarget->exprs =
+ lappend(joinrel->reltarget->exprs, param);
+
+ /*
+ * Estimate using the type info (Note: keep this in sync with
+ * set_rel_width())
+ */
+ joinrel->reltarget->width +=
+ get_typavgwidth(param->paramtype, param->paramtypmod);
+
+ continue;
+ }
/*
* Otherwise, anything in a baserel or joinrel targetlist ought to be
diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c
index 88de5c0481c..004f22084f8 100644
--- a/src/backend/utils/adt/ruleutils.c
+++ b/src/backend/utils/adt/ruleutils.c
@@ -9374,7 +9374,7 @@ get_parameter(Param *param, deparse_context *context)
* It's a bug if we get here for anything except PARAM_EXTERN Params, but
* in production builds printing $N seems more useful than failing.
*/
- Assert(param->paramkind == PARAM_EXTERN);
+ Assert(param->paramkind == PARAM_EXTERN || param->paramkind == PARAM_EXEC);
appendStringInfo(context->buf, "$%d", param->paramid);
}
diff --git a/src/include/executor/tuptable.h b/src/include/executor/tuptable.h
index 43018ed4dfa..02ddc4fb79d 100644
--- a/src/include/executor/tuptable.h
+++ b/src/include/executor/tuptable.h
@@ -141,6 +141,7 @@ typedef struct TupleTableSlot
MemoryContext tts_mcxt; /* slot itself is in this context */
ItemPointerData tts_tid; /* stored tuple's tid */
Oid tts_tableOid; /* table oid of tuple */
+ Oid tts_remoteOid;
} TupleTableSlot;
/* routines for a TupleTableSlot implementation */
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 27a2c6815b7..abe4904cc24 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -271,12 +271,19 @@ typedef struct PlannerGlobal
/* extension state */
void **extension_state pg_node_attr(read_write_ignore);
int extension_state_allocated;
+
+ /* PARAM_EXEC Params generated by FDWs */
+ Bitmapset *foreignParamIDs;
} PlannerGlobal;
/* macro for fetching the Plan associated with a SubPlan node */
#define planner_subplan_get_plan(root, subplan) \
((Plan *) list_nth((root)->glob->subplans, (subplan)->plan_id - 1))
+/* macro for checking if a Param is a PARAM_EXEC Param generated by an FDW */
+#define IS_FOREIGN_PARAM(root, param) \
+ ((param)->paramkind == PARAM_EXEC && \
+ bms_is_member((param)->paramid, (root)->glob->foreignParamIDs))
/*----------
* PlannerInfo
diff --git a/src/include/nodes/primnodes.h b/src/include/nodes/primnodes.h
index 7977ee24783..cd70f673236 100644
--- a/src/include/nodes/primnodes.h
+++ b/src/include/nodes/primnodes.h
@@ -402,6 +402,7 @@ typedef struct Param
Oid paramcollid;
/* token location, or -1 if unknown */
ParseLoc location;
+ Index target_rte;
} Param;
/*
--
2.43.0