Skip to content

Commit c25e638

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 fc732e0 commit c25e638

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
@@ -836,6 +836,55 @@ foreign_expr_walker(Node *node,
836836
return true;
837837
}
838838

839+
/*
840+
* Returns true if given expr is something we'd have to send the value of
841+
* to the foreign server.
842+
*
843+
* This should return true when the expression is a shippable node that
844+
* deparseExpr would add to context->params_list. Note that we don't care
845+
* if the expression *contains* such a node, only whether one appears at top
846+
* level. We need this to detect cases where setrefs.c would recognize a
847+
* false match between an fdw_exprs item (which came from the params_list)
848+
* and an entry in fdw_scan_tlist (which we're considering putting the given
849+
* expression into).
850+
*/
851+
bool
852+
is_foreign_param(PlannerInfo *root,
853+
RelOptInfo *baserel,
854+
Expr *expr)
855+
{
856+
if (expr == NULL)
857+
return false;
858+
859+
switch (nodeTag(expr))
860+
{
861+
case T_Var:
862+
{
863+
/* It would have to be sent unless it's a foreign Var */
864+
Var *var = (Var *) expr;
865+
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) (baserel->fdw_private);
866+
Relids relids;
867+
868+
if (IS_UPPER_REL(baserel))
869+
relids = fpinfo->outerrel->relids;
870+
else
871+
relids = baserel->relids;
872+
873+
if (bms_is_member(var->varno, relids) && var->varlevelsup == 0)
874+
return false; /* foreign Var, so not a param */
875+
else
876+
return true; /* it'd have to be a param */
877+
break;
878+
}
879+
case T_Param:
880+
/* Params always have to be sent to the foreign server */
881+
return true;
882+
default:
883+
break;
884+
}
885+
return false;
886+
}
887+
839888
/*
840889
* Convert type OID + typmod info into a type name we can ship to the remote
841890
* 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
@@ -2814,6 +2814,46 @@ select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100
28142814
Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1"
28152815
(10 rows)
28162816

2817+
-- Remote aggregate in combination with a local Param (for the output
2818+
-- of an initplan) can be trouble, per bug #15781
2819+
explain (verbose, costs off)
2820+
select exists(select 1 from pg_enum), sum(c1) from ft1;
2821+
QUERY PLAN
2822+
--------------------------------------------------
2823+
Foreign Scan
2824+
Output: $0, (sum(ft1.c1))
2825+
Relations: Aggregate on (public.ft1)
2826+
Remote SQL: SELECT sum("C 1") FROM "S 1"."T 1"
2827+
InitPlan 1 (returns $0)
2828+
-> Seq Scan on pg_catalog.pg_enum
2829+
(6 rows)
2830+
2831+
select exists(select 1 from pg_enum), sum(c1) from ft1;
2832+
exists | sum
2833+
--------+--------
2834+
t | 500500
2835+
(1 row)
2836+
2837+
explain (verbose, costs off)
2838+
select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
2839+
QUERY PLAN
2840+
---------------------------------------------------
2841+
GroupAggregate
2842+
Output: ($0), sum(ft1.c1)
2843+
Group Key: $0
2844+
InitPlan 1 (returns $0)
2845+
-> Seq Scan on pg_catalog.pg_enum
2846+
-> Foreign Scan on public.ft1
2847+
Output: $0, ft1.c1
2848+
Remote SQL: SELECT "C 1" FROM "S 1"."T 1"
2849+
(8 rows)
2850+
2851+
select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
2852+
exists | sum
2853+
--------+--------
2854+
t | 500500
2855+
(1 row)
2856+
28172857
-- Testing ORDER BY, DISTINCT, FILTER, Ordered-sets and VARIADIC within aggregates
28182858
-- ORDER BY within aggregate, same column used to order
28192859
explain (verbose, costs off)

contrib/postgres_fdw/postgres_fdw.c

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4637,7 +4637,6 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
46374637
PathTarget *grouping_target = root->upper_targets[UPPERREL_GROUP_AGG];
46384638
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) grouped_rel->fdw_private;
46394639
PgFdwRelationInfo *ofpinfo;
4640-
List *aggvars;
46414640
ListCell *lc;
46424641
int i;
46434642
List *tlist = NIL;
@@ -4663,6 +4662,15 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
46634662
* server. All GROUP BY expressions will be part of the grouping target
46644663
* and thus there is no need to search for them separately. Add grouping
46654664
* expressions into target list which will be passed to foreign server.
4665+
*
4666+
* A tricky fine point is that we must not put any expression into the
4667+
* target list that is just a foreign param (that is, something that
4668+
* deparse.c would conclude has to be sent to the foreign server). If we
4669+
* do, the expression will also appear in the fdw_exprs list of the plan
4670+
* node, and setrefs.c will get confused and decide that the fdw_exprs
4671+
* entry is actually a reference to the fdw_scan_tlist entry, resulting in
4672+
* a broken plan. Somewhat oddly, it's OK if the expression contains such
4673+
* a node, as long as it's not at top level; then no match is possible.
46664674
*/
46674675
i = 0;
46684676
foreach(lc, grouping_target->exprs)
@@ -4683,6 +4691,13 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
46834691
if (!is_foreign_expr(root, grouped_rel, expr))
46844692
return false;
46854693

4694+
/*
4695+
* If it would be a foreign param, we can't put it into the tlist,
4696+
* so we have to fail.
4697+
*/
4698+
if (is_foreign_param(root, grouped_rel, expr))
4699+
return false;
4700+
46864701
/*
46874702
* Pushable, so add to tlist. We need to create a TLE for this
46884703
* expression and apply the sortgroupref to it. We cannot use
@@ -4698,22 +4713,28 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
46984713
else
46994714
{
47004715
/*
4701-
* Non-grouping expression we need to compute. Is it shippable?
4716+
* Non-grouping expression we need to compute. Can we ship it
4717+
* as-is to the foreign server?
47024718
*/
4703-
if (is_foreign_expr(root, grouped_rel, expr))
4719+
if (is_foreign_expr(root, grouped_rel, expr) &&
4720+
!is_foreign_param(root, grouped_rel, expr))
47044721
{
47054722
/* Yes, so add to tlist as-is; OK to suppress duplicates */
47064723
tlist = add_to_flat_tlist(tlist, list_make1(expr));
47074724
}
47084725
else
47094726
{
47104727
/* Not pushable as a whole; extract its Vars and aggregates */
4728+
List *aggvars;
4729+
47114730
aggvars = pull_var_clause((Node *) expr,
47124731
PVC_INCLUDE_AGGREGATES);
47134732

47144733
/*
47154734
* If any aggregate expression is not shippable, then we
4716-
* cannot push down aggregation to the foreign server.
4735+
* cannot push down aggregation to the foreign server. (We
4736+
* don't have to check is_foreign_param, since that certainly
4737+
* won't return true for any such expression.)
47174738
*/
47184739
if (!is_foreign_expr(root, grouped_rel, (Expr *) aggvars))
47194740
return false;
@@ -4800,7 +4821,8 @@ foreign_grouping_ok(PlannerInfo *root, RelOptInfo *grouped_rel)
48004821
* If aggregates within local conditions are not safe to push
48014822
* down, then we cannot push down the query. Vars are already
48024823
* part of GROUP BY clause which are checked above, so no need to
4803-
* access them again here.
4824+
* access them again here. Again, we need not check
4825+
* is_foreign_param for a foreign aggregate.
48044826
*/
48054827
if (IsA(expr, Aggref))
48064828
{

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, PlannerInfo *root,
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
@@ -668,6 +668,16 @@ select count(*) from (select c5, count(c1) from ft1 group by c5, sqrt(c2) having
668668
explain (verbose, costs off)
669669
select sum(c1) from ft1 group by c2 having avg(c1 * (random() <= 1)::int) > 100 order by 1;
670670

671+
-- Remote aggregate in combination with a local Param (for the output
672+
-- of an initplan) can be trouble, per bug #15781
673+
explain (verbose, costs off)
674+
select exists(select 1 from pg_enum), sum(c1) from ft1;
675+
select exists(select 1 from pg_enum), sum(c1) from ft1;
676+
677+
explain (verbose, costs off)
678+
select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
679+
select exists(select 1 from pg_enum), sum(c1) from ft1 group by 1;
680+
671681

672682
-- Testing ORDER BY, DISTINCT, FILTER, Ordered-sets and VARIADIC within aggregates
673683

0 commit comments

Comments
 (0)