Skip to content

Commit 1bf52d6

Browse files
committed
Avoid postgres_fdw crash for a targetlist entry that's just a Param.
foreign_grouping_ok() is willing to put fairly arbitrary expressions into the targetlist of a remote SELECT that's doing grouping or aggregation on the remote side, including expressions that have no foreign component to them at all. This is possibly a bit dubious from an efficiency standpoint; but it rises to the level of a crash-causing bug if the expression is just a Param or non-foreign Var. In that case, the expression will necessarily also appear in the fdw_exprs list of values we need to send to the remote server, and then setrefs.c's set_foreignscan_references will mistakenly replace the fdw_exprs entry with a Var referencing the targetlist result. The root cause of this problem is bad design in commit e7cb7ee: it put logic into set_foreignscan_references that IMV is postgres_fdw-specific, and yet this bug shows that it isn't postgres_fdw-specific enough. The transformation being done on fdw_exprs assumes that fdw_exprs is to be evaluated with the fdw_scan_tlist as input, which is not how postgres_fdw uses it; yet it could be the right thing for some other FDW. (In the bigger picture, setrefs.c has no business assuming this for the other expression fields of a ForeignScan either.) The right fix therefore would be to expand the FDW API so that the FDW could inform setrefs.c how it intends to evaluate these various expressions. We can't change that in the back branches though, and we also can't just summarily change setrefs.c's behavior there, or we're likely to break external FDWs. As a stopgap, therefore, hack up postgres_fdw so that it won't attempt to send targetlist entries that look exactly like the fdw_exprs entries they'd produce. In most cases this actually produces a superior plan, IMO, with less data needing to be transmitted and returned; so we probably ought to think harder about whether we should ship tlist expressions at all when they don't contain any foreign Vars or Aggs. But that's an optimization not a bug fix so I left it for later. One case where this produces an inferior plan is where the expression in question is actually a GROUP BY expression: then the restriction prevents us from using remote grouping. It might be possible to work around that (since that would reduce to group-by-a-constant on the remote side); but it seems like a pretty unlikely corner case, so I'm not sure it's worth expending effort solely to improve that. In any case the right long-term answer is to fix the API as sketched above, and then revert this hack. Per bug #15781 from Sean Johnston. Back-patch to v10 where the problem was introduced. Discussion: https://postgr.es/m/15781-2601b1002bad087c@postgresql.org
1 parent d51cfb0 commit 1bf52d6

File tree

5 files changed

+129
-5
lines changed

5 files changed

+129
-5
lines changed

contrib/postgres_fdw/deparse.c

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,55 @@ foreign_expr_walker(Node *node,
840840
return true;
841841
}
842842

843+
/*
844+
* Returns true if given expr is something we'd have to send the value of
845+
* to the foreign server.
846+
*
847+
* This should return true when the expression is a shippable node that
848+
* deparseExpr would add to context->params_list. Note that we don't care
849+
* if the expression *contains* such a node, only whether one appears at top
850+
* level. We need this to detect cases where setrefs.c would recognize a
851+
* false match between an fdw_exprs item (which came from the params_list)
852+
* and an entry in fdw_scan_tlist (which we're considering putting the given
853+
* expression into).
854+
*/
855+
bool
856+
is_foreign_param(PlannerInfo *root,
857+
RelOptInfo *baserel,
858+
Expr *expr)
859+
{
860+
if (expr == NULL)
861+
return false;
862+
863+
switch (nodeTag(expr))
864+
{
865+
case T_Var:
866+
{
867+
/* It would have to be sent unless it's a foreign Var */
868+
Var *var = (Var *) expr;
869+
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) (baserel->fdw_private);
870+
Relids relids;
871+
872+
if (IS_UPPER_REL(baserel))
873+
relids = fpinfo->outerrel->relids;
874+
else
875+
relids = baserel->relids;
876+
877+
if (bms_is_member(var->varno, relids) && var->varlevelsup == 0)
878+
return false; /* foreign Var, so not a param */
879+
else
880+
return true; /* it'd have to be a param */
881+
break;
882+
}
883+
case T_Param:
884+
/* Params always have to be sent to the foreign server */
885+
return true;
886+
default:
887+
break;
888+
}
889+
return false;
890+
}
891+
843892
/*
844893
* Convert type OID + typmod info into a type name we can ship to the remote
845894
* server. Someplace else had better have verified that this type name is

contrib/postgres_fdw/expected/postgres_fdw.out

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2827,6 +2827,46 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
28272827
Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
28282828
(10 rows)
28292829

2830+
-- Remote aggregate in combination with a local Param (for the output
2831+
-- of an initplan) can be trouble, per bug #15781
2832+
explain (verbose, costs off)
2833+
select exists(select 1 from pg_enum), sum(c1) from ft1;
2834+
QUERY PLAN
2835+
--------------------------------------------------
2836+
Foreign Scan
2837+
Output: $0, (sum(ft1.c1))
2838+
Relations: Aggregate on (public.ft1)
2839+
Remote SQL: SELECT sum("C 1") FROM "S 1"."T 1"
2840+
InitPlan 1 (returns $0)
2841+
-> Seq Scan on pg_catalog.pg_enum
2842+
(6 rows)
2843+
2844+
select exists(select 1 from pg_enum), sum(c1) from ft1;
2845+
exists | sum
2846+
--------+--------
2847+
t | 500500
2848+
(1 row)
2849+
2850+
explain (verbose, costs off)
2851+
select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
2852+
QUERY PLAN
2853+
---------------------------------------------------
2854+
GroupAggregate
2855+
Output: ($0), sum(ft1.c1)
2856+
Group Key: $0
2857+
InitPlan 1 (returns $0)
2858+
-> Seq Scan on pg_catalog.pg_enum
2859+
-> Foreign Scan on public.ft1
2860+
Output: $0, ft1.c1
2861+
Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
2862+
(8 rows)
2863+
2864+
select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
2865+
exists | sum
2866+
--------+--------
2867+
t | 500500
2868+
(1 row)
2869+
28302870
-- Testing ORDER BY, DISTINCT, FILTER, Ordered-sets and VARIADIC within aggregates
28312871
-- ORDER BY within aggregate, same column used to order
28322872
explain (verbose, costs off)

contrib/postgres_fdw/postgres_fdw.c

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5291,7 +5291,6 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
52915291
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) grouped_rel->fdw_private;
52925292
PathTarget *grouping_target = grouped_rel->reltarget;
52935293
PgFdwRelationInfo *ofpinfo;
5294-
List *aggvars;
52955294
ListCell *lc;
52965295
int i;
52975296
List *tlist = NIL;
@@ -5317,6 +5316,15 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
53175316
* server. All GROUP BY expressions will be part of the grouping target
53185317
* and thus there is no need to search for them separately. Add grouping
53195318
* expressions into target list which will be passed to foreign server.
5319+
*
5320+
* A tricky fine point is that we must not put any expression into the
5321+
* target list that is just a foreign param (that is, something that
5322+
* deparse.c would conclude has to be sent to the foreign server). If we
5323+
* do, the expression will also appear in the fdw_exprs list of the plan
5324+
* node, and setrefs.c will get confused and decide that the fdw_exprs
5325+
* entry is actually a reference to the fdw_scan_tlist entry, resulting in
5326+
* a broken plan. Somewhat oddly, it's OK if the expression contains such
5327+
* a node, as long as it's not at top level; then no match is possible.
53205328
*/
53215329
i = 0;
53225330
foreach(lc, grouping_target->exprs)
@@ -5337,6 +5345,13 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
53375345
if (!is_foreign_expr(root, grouped_rel, expr))
53385346
return false;
53395347

5348+
/*
5349+
* If it would be a foreign param, we can't put it into the tlist,
5350+
* so we have to fail.
5351+
*/
5352+
if (is_foreign_param(root, grouped_rel, expr))
5353+
return false;
5354+
53405355
/*
53415356
* Pushable, so add to tlist. We need to create a TLE for this
53425357
* expression and apply the sortgroupref to it. We cannot use
@@ -5352,22 +5367,28 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
53525367
else
53535368
{
53545369
/*
5355-
* Non-grouping expression we need to compute. Is it shippable?
5370+
* Non-grouping expression we need to compute. Can we ship it
5371+
* as-is to the foreign server?
53565372
*/
5357-
if (is_foreign_expr(root, grouped_rel, expr))
5373+
if (is_foreign_expr(root, grouped_rel, expr) &&
5374+
!is_foreign_param(root, grouped_rel, expr))
53585375
{
53595376
/* Yes, so add to tlist as-is; OK to suppress duplicates */
53605377
tlist = add_to_flat_tlist(tlist, list_make1(expr));
53615378
}
53625379
else
53635380
{
53645381
/* Not pushable as a whole; extract its Vars and aggregates */
5382+
List *aggvars;
5383+
53655384
aggvars = pull_var_clause((Node *) expr,
53665385
PVC_INCLUDE_AGGREGATES);
53675386

53685387
/*
53695388
* If any aggregate expression is not shippable, then we
5370-
* cannot push down aggregation to the foreign server.
5389+
* cannot push down aggregation to the foreign server. (We
5390+
* don't have to check is_foreign_param, since that certainly
5391+
* won't return true for any such expression.)
53715392
*/
53725393
if (!is_foreign_expr(root, grouped_rel, (Expr *) aggvars))
53735394
return false;
@@ -5454,7 +5475,8 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel,
54545475
* If aggregates within local conditions are not safe to push
54555476
* down, then we cannot push down the query. Vars are already
54565477
* part of GROUP BY clause which are checked above, so no need to
5457-
* access them again here.
5478+
* access them again here. Again, we need not check
5479+
* is_foreign_param for a foreign aggregate.
54585480
*/
54595481
if (IsA(expr, Aggref))
54605482
{

contrib/postgres_fdw/postgres_fdw.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ extern void classifyConditions(PlannerInfo *root,
140140
extern bool is_foreign_expr(PlannerInfo *root,
141141
RelOptInfo *baserel,
142142
Expr *expr);
143+
extern bool is_foreign_param(PlannerInfo *root,
144+
RelOptInfo *baserel,
145+
Expr *expr);
143146
extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
144147
Index rtindex, Relation rel,
145148
List *targetAttrs, bool doNothing, List *returningList,

contrib/postgres_fdw/sql/postgres_fdw.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,16 @@ select count(*) from (select c5, count(c1) from ft1 group by c5, sqrt(c2) having
674674
explain (verbose, costs off)
675675
select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100 order by 1;
676676

677+
-- Remote aggregate in combination with a local Param (for the output
678+
-- of an initplan) can be trouble, per bug #15781
679+
explain (verbose, costs off)
680+
select exists(select 1 from pg_enum), sum(c1) from ft1;
681+
select exists(select 1 from pg_enum), sum(c1) from ft1;
682+
683+
explain (verbose, costs off)
684+
select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
685+
select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
686+
677687

678688
-- Testing ORDER BY, DISTINCT, FILTER, Ordered-sets and VARIADIC within aggregates
679689

0 commit comments

Comments
 (0)