From 26676f88ad8fbb70ff1ca80d4fc37d14c5e07797 Mon Sep 17 00:00:00 2001 From: Ajit Awekar Date: Tue, 3 Nov 2026 12:25:08 +0530 Subject: [PATCH v5 1/2] Allow FDWs to add row-identity pseudo-columns to a foreign rel's targetlist An FDW can now add a Var with an attribute number beyond the relation's max_attr (for use as a row-identity column for UPDATE/DELETE). Such a Var has no attr_needed[] or attr_widths[] slot, so teach the planner to cope with it: * add_vars_to_targetlist() adds it to the rel's reltarget directly. * set_rel_width() and build_joinrel_tlist() estimate its width from its type, and treat it as needed above every join, like a ROWID_VAR. * set_plan_refs() replaces it with a NULL constant in a dummy Result, as is done for ROWID_VARs. * ruleutils.c prints its fdw_scan_tlist name in EXPLAIN rather than looking up a nonexistent attribute. These code paths are reached only if an FDW injects such a Var; an unexpected out-of-range attno on a non-foreign relation raises an error. No behavior changes unless an FDW uses this, as the following postgres_fdw commit does. Author: Nikita Malakhov Author: Ajit Awekar Suggested-by: Robert Haas Reviewed-by: Jakub Wartak Reviewed-by: Manuel Reyes Bravo Discussion: https://postgr.es/m/20250718175314.4513c00a@karst --- src/backend/optimizer/path/costsize.c | 16 ++++++++++ src/backend/optimizer/plan/initsplan.c | 21 +++++++++++++ src/backend/optimizer/plan/setrefs.c | 10 +++++- src/backend/optimizer/util/relnode.c | 42 +++++++++++++++++++------- src/backend/utils/adt/ruleutils.c | 23 ++++++++++++++ 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 7bbddb8bee4..5795ad3f409 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -6493,6 +6493,22 @@ set_rel_width(PlannerInfo *root, RelOptInfo *rel) int ndx; int32 item_width; + /* + * An out-of-range attno (> max_attr) is an FDW row-identity + * pseudo-column (postgres_fdw's remote table OID for + * UPDATE/DELETE; see add_vars_to_targetlist()) with no + * attr_widths[] slot; estimate its width from the type. + */ + if (var->varattno > rel->max_attr) + { + /* Only an FDW can inject an out-of-range row-identity column */ + if (rel->fdwroutine == NULL) + elog(ERROR, "bogus varattno %d for relation %u: not a foreign table", + var->varattno, rel->relid); + tuple_width += get_typavgwidth(var->vartype, var->vartypmod); + continue; + } + Assert(var->varattno >= rel->min_attr); Assert(var->varattno <= rel->max_attr); diff --git a/src/backend/optimizer/plan/initsplan.c b/src/backend/optimizer/plan/initsplan.c index cec76342f6e..b94b60daa27 100644 --- a/src/backend/optimizer/plan/initsplan.c +++ b/src/backend/optimizer/plan/initsplan.c @@ -314,6 +314,27 @@ add_vars_to_targetlist(PlannerInfo *root, List *vars, if (bms_is_subset(where_needed, rel->relids)) continue; + + /* + * An out-of-range attno (> max_attr) is an FDW row-identity + * pseudo-column (e.g. postgres_fdw's remote table OID for + * UPDATE/DELETE) with no attr_needed[] slot; like a ROWID_VAR + * it's always needed, so just add it to the reltarget once. + */ + if (attno > rel->max_attr) + { + /* Only an FDW can inject an out-of-range row-identity column */ + if (rel->fdwroutine == NULL) + elog(ERROR, "bogus varattno %d for relation %u: not a foreign table", + attno, rel->relid); + var = copyObject(var); + var->varnullingrels = NULL; + if (!list_member(rel->reltarget->exprs, var)) + rel->reltarget->exprs = lappend(rel->reltarget->exprs, + var); + continue; + } + Assert(attno >= rel->min_attr && attno <= rel->max_attr); attno -= rel->min_attr; if (rel->attr_needed[attno] == NULL) diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index af78892fafc..ff856df52c7 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -15,6 +15,7 @@ */ #include "postgres.h" +#include "access/htup_details.h" #include "access/transam.h" #include "catalog/pg_type.h" #include "nodes/makefuncs.h" @@ -1058,6 +1059,11 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) * it here and keep the assertions that ROWID_VARs * shouldn't be seen by fix_scan_expr. * + * The same applies to an FDW row-identity pseudo-column + * (attno > MaxHeapAttributeNumber; see + * add_vars_to_targetlist()): no scan node exists here to + * give it a value or a name, so replace it the same way. + * * We also must handle the case where set operations have * been proven empty, resulting in a dummy Result node. * Because prepunion.c uses varno 0 for setop targetlists, @@ -1080,7 +1086,9 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) TargetEntry *tle = (TargetEntry *) lfirst(l); Var *var = (Var *) tle->expr; - if (var && IsA(var, Var) && var->varno == ROWID_VAR) + if (var && IsA(var, Var) && + (var->varno == ROWID_VAR || + var->varattno > MaxHeapAttributeNumber)) tle->expr = (Expr *) makeNullConst(var->vartype, var->vartypmod, var->varcollid); diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c index 90a687c32da..251ad9239fb 100644 --- a/src/backend/optimizer/util/relnode.c +++ b/src/backend/optimizer/util/relnode.c @@ -1305,6 +1305,7 @@ build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel, foreach(vars, input_rel->reltarget->exprs) { Var *var = (Var *) lfirst(vars); + RelOptInfo *baserel = NULL; /* * For a PlaceHolderVar, we have to look up the PlaceHolderInfo. @@ -1376,28 +1377,47 @@ build_joinrel_tlist(PlannerInfo *root, RelOptInfo *joinrel, } else { - RelOptInfo *baserel; - int ndx; - /* Get the Var's original base rel */ baserel = find_base_rel(root, var->varno); - /* Is it still needed above this joinrel? */ - ndx = var->varattno - baserel->min_attr; - if (!bms_nonempty_difference(baserel->attr_needed[ndx], relids)) - continue; /* nope, skip it */ + if (var->varattno > baserel->max_attr) + { + /* + * An out-of-range attno (> max_attr) is an FDW row-identity + * pseudo-column (postgres_fdw's remote table OID for + * UPDATE/DELETE; see add_vars_to_targetlist()) with no + * attr_needed[]/attr_widths[] slot; like a ROWID_VAR, always + * needed above every join. + */ + if (baserel->fdwroutine == NULL) + elog(ERROR, "bogus varattno %d for relation %u: not a foreign table", + var->varattno, baserel->relid); + tuple_width += get_typavgwidth(var->vartype, var->vartypmod); + } + else + { + int ndx; - /* Update reltarget width estimate from baserel's attr_widths */ - tuple_width += baserel->attr_widths[ndx]; + /* Is it still needed above this joinrel? */ + ndx = var->varattno - baserel->min_attr; + if (!bms_nonempty_difference(baserel->attr_needed[ndx], relids)) + continue; /* nope, skip it */ + + /* Update reltarget width estimate from baserel's attr_widths */ + tuple_width += baserel->attr_widths[ndx]; + } } /* * Add the Var to the output. If this join potentially nulls this * input, we have to update the Var's varnullingrels, which means * making a copy. But note that we don't ever add nullingrel bits to - * row identity Vars (cf. comments in setrefs.c). + * row identity Vars (cf. comments in setrefs.c); that includes the + * FDW row-identity pseudo-columns handled just above, which have an + * out-of-range attribute number. */ - if (can_null && var->varno != ROWID_VAR) + if (can_null && var->varno != ROWID_VAR && + var->varattno <= baserel->max_attr) { var = copyObject(var); /* See comments above to understand this logic */ diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 8f6a46fa614..87c37b90777 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -8022,6 +8022,29 @@ resolve_special_varno(Node *node, deparse_context *context, if (!tle) elog(ERROR, "bogus varattno for INDEX_VAR var: %d", var->varattno); + /* + * A row-identity pseudo-column in fdw_scan_tlist has an out-of-range + * attno and no catalog column to name, so when deparsing it as a + * variable (get_special_variable) print its scan-tlist name rather + * than chasing a non-existent attribute (which fails in + * get_variable). + */ + if (callback == get_special_variable && tle->resname && + IsA(tle->expr, Var)) + { + Var *itvar = (Var *) tle->expr; + + if (itvar->varno >= 1 && + itvar->varno <= list_length(dpns->rtable) && + itvar->varattno > + deparse_columns_fetch(itvar->varno, dpns)->num_cols) + { + appendStringInfoString(context->buf, + quote_identifier(tle->resname)); + return; + } + } + resolve_special_varno((Node *) tle->expr, context, callback, callback_arg); return; -- 2.52.0