Skip to content

Commit b9eb041

Browse files
committed
Fix postgres_fdw to check shippability of sort clauses properly.
postgres_fdw would push ORDER BY clauses to the remote side without verifying that the sort operator is safe to ship. Moreover, it failed to print a suitable USING clause if the sort operator isn't default for the sort expression's type. The net result of this is that the remote sort might not have anywhere near the semantics we expect, which'd be disastrous for locally-performed merge joins in particular. We addressed similar issues in the context of ORDER BY within an aggregate function call in commit 7012b13, but failed to notice that query-level ORDER BY was broken. Thus, much of the necessary logic already existed, but it requires refactoring to be usable in both cases. Back-patch to all supported branches. In HEAD only, remove the core code's copy of find_em_expr_for_rel, which is no longer used and really should never have been pushed into equivclass.c in the first place. Ronan Dunklau, per report from David Rowley; reviews by David Rowley, Ranier Vilela, and myself Discussion: https://postgr.es/m/CAApHDvr4OeC2DBVY--zVP83-K=bYrTD7F8SZDhN4g+pj2f2S-A@mail.gmail.com
1 parent 1430431 commit b9eb041

File tree

5 files changed

+181
-73
lines changed

5 files changed

+181
-73
lines changed

contrib/postgres_fdw/deparse.c

Lines changed: 114 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include "catalog/pg_collation.h"
4343
#include "catalog/pg_namespace.h"
4444
#include "catalog/pg_operator.h"
45+
#include "catalog/pg_opfamily.h"
4546
#include "catalog/pg_proc.h"
4647
#include "catalog/pg_type.h"
4748
#include "commands/defrem.h"
@@ -179,6 +180,8 @@ static void deparseRangeTblRef(StringInfo buf, PlannerInfo *root,
179180
Index ignore_rel, List **ignore_conds, List **params_list);
180181
static void deparseAggref(Aggref *node, deparse_expr_cxt *context);
181182
static void appendGroupByClause(List *tlist, deparse_expr_cxt *context);
183+
static void appendOrderBySuffix(Oid sortop, Oid sortcoltype, bool nulls_first,
184+
deparse_expr_cxt *context);
182185
static void appendAggOrderBy(List *orderList, List *targetList,
183186
deparse_expr_cxt *context);
184187
static void appendFunctionName(Oid funcid, deparse_expr_cxt *context);
@@ -905,6 +908,33 @@ is_foreign_param(PlannerInfo *root,
905908
return false;
906909
}
907910

911+
/*
912+
* Returns true if it's safe to push down the sort expression described by
913+
* 'pathkey' to the foreign server.
914+
*/
915+
bool
916+
is_foreign_pathkey(PlannerInfo *root,
917+
RelOptInfo *baserel,
918+
PathKey *pathkey)
919+
{
920+
EquivalenceClass *pathkey_ec = pathkey->pk_eclass;
921+
PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) baserel->fdw_private;
922+
923+
/*
924+
* is_foreign_expr would detect volatile expressions as well, but checking
925+
* ec_has_volatile here saves some cycles.
926+
*/
927+
if (pathkey_ec->ec_has_volatile)
928+
return false;
929+
930+
/* can't push down the sort if the pathkey's opfamily is not shippable */
931+
if (!is_shippable(pathkey->pk_opfamily, OperatorFamilyRelationId, fpinfo))
932+
return false;
933+
934+
/* can push if a suitable EC member exists */
935+
return (find_em_for_rel(root, pathkey_ec, baserel) != NULL);
936+
}
937+
908938
/*
909939
* Convert type OID + typmod info into a type name we can ship to the remote
910940
* server. Someplace else had better have verified that this type name is
@@ -3054,44 +3084,59 @@ appendAggOrderBy(List *orderList, List *targetList, deparse_expr_cxt *context)
30543084
{
30553085
SortGroupClause *srt = (SortGroupClause *) lfirst(lc);
30563086
Node *sortexpr;
3057-
Oid sortcoltype;
3058-
TypeCacheEntry *typentry;
30593087

30603088
if (!first)
30613089
appendStringInfoString(buf, ", ");
30623090
first = false;
30633091

3092+
/* Deparse the sort expression proper. */
30643093
sortexpr = deparseSortGroupClause(srt->tleSortGroupRef, targetList,
30653094
false, context);
3066-
sortcoltype = exprType(sortexpr);
3067-
/* See whether operator is default < or > for datatype */
3068-
typentry = lookup_type_cache(sortcoltype,
3069-
TYPECACHE_LT_OPR | TYPECACHE_GT_OPR);
3070-
if (srt->sortop == typentry->lt_opr)
3071-
appendStringInfoString(buf, " ASC");
3072-
else if (srt->sortop == typentry->gt_opr)
3073-
appendStringInfoString(buf, " DESC");
3074-
else
3075-
{
3076-
HeapTuple opertup;
3077-
Form_pg_operator operform;
3078-
3079-
appendStringInfoString(buf, " USING ");
3080-
3081-
/* Append operator name. */
3082-
opertup = SearchSysCache1(OPEROID, ObjectIdGetDatum(srt->sortop));
3083-
if (!HeapTupleIsValid(opertup))
3084-
elog(ERROR, "cache lookup failed for operator %u", srt->sortop);
3085-
operform = (Form_pg_operator) GETSTRUCT(opertup);
3086-
deparseOperatorName(buf, operform);
3087-
ReleaseSysCache(opertup);
3088-
}
3095+
/* Add decoration as needed. */
3096+
appendOrderBySuffix(srt->sortop, exprType(sortexpr), srt->nulls_first,
3097+
context);
3098+
}
3099+
}
30893100

3090-
if (srt->nulls_first)
3091-
appendStringInfoString(buf, " NULLS FIRST");
3092-
else
3093-
appendStringInfoString(buf, " NULLS LAST");
3101+
/*
3102+
* Append the ASC, DESC, USING <OPERATOR> and NULLS FIRST / NULLS LAST parts
3103+
* of an ORDER BY clause.
3104+
*/
3105+
static void
3106+
appendOrderBySuffix(Oid sortop, Oid sortcoltype, bool nulls_first,
3107+
deparse_expr_cxt *context)
3108+
{
3109+
StringInfo buf = context->buf;
3110+
TypeCacheEntry *typentry;
3111+
3112+
/* See whether operator is default < or > for sort expr's datatype. */
3113+
typentry = lookup_type_cache(sortcoltype,
3114+
TYPECACHE_LT_OPR | TYPECACHE_GT_OPR);
3115+
3116+
if (sortop == typentry->lt_opr)
3117+
appendStringInfoString(buf, " ASC");
3118+
else if (sortop == typentry->gt_opr)
3119+
appendStringInfoString(buf, " DESC");
3120+
else
3121+
{
3122+
HeapTuple opertup;
3123+
Form_pg_operator operform;
3124+
3125+
appendStringInfoString(buf, " USING ");
3126+
3127+
/* Append operator name. */
3128+
opertup = SearchSysCache1(OPEROID, ObjectIdGetDatum(sortop));
3129+
if (!HeapTupleIsValid(opertup))
3130+
elog(ERROR, "cache lookup failed for operator %u", sortop);
3131+
operform = (Form_pg_operator) GETSTRUCT(opertup);
3132+
deparseOperatorName(buf, operform);
3133+
ReleaseSysCache(opertup);
30943134
}
3135+
3136+
if (nulls_first)
3137+
appendStringInfoString(buf, " NULLS FIRST");
3138+
else
3139+
appendStringInfoString(buf, " NULLS LAST");
30953140
}
30963141

30973142
/*
@@ -3174,17 +3219,17 @@ appendGroupByClause(List *tlist, deparse_expr_cxt *context)
31743219
}
31753220

31763221
/*
3177-
* Deparse ORDER BY clause according to the given pathkeys for given base
3178-
* relation. From given pathkeys expressions belonging entirely to the given
3179-
* base relation are obtained and deparsed.
3222+
* Deparse ORDER BY clause defined by the given pathkeys.
3223+
*
3224+
* We find a suitable pathkey expression (some earlier step
3225+
* should have verified that there is one) and deparse it.
31803226
*/
31813227
static void
31823228
appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
31833229
{
31843230
ListCell *lcell;
31853231
int nestlevel;
3186-
char *delim = " ";
3187-
RelOptInfo *baserel = context->scanrel;
3232+
const char *delim = " ";
31883233
StringInfo buf = context->buf;
31893234

31903235
/* Make sure any constants in the exprs are printed portably */
@@ -3194,22 +3239,47 @@ appendOrderByClause(List *pathkeys, deparse_expr_cxt *context)
31943239
foreach(lcell, pathkeys)
31953240
{
31963241
PathKey *pathkey = lfirst(lcell);
3242+
EquivalenceMember *em;
31973243
Expr *em_expr;
3244+
Oid oprid;
31983245

3199-
em_expr = find_em_expr_for_rel(pathkey->pk_eclass, baserel);
3200-
Assert(em_expr != NULL);
3246+
em = find_em_for_rel(context->root,
3247+
pathkey->pk_eclass,
3248+
context->scanrel);
3249+
3250+
/*
3251+
* We don't expect any error here; it would mean that shippability
3252+
* wasn't verified earlier. For the same reason, we don't recheck
3253+
* shippability of the sort operator.
3254+
*/
3255+
if (em == NULL)
3256+
elog(ERROR, "could not find pathkey item to sort");
3257+
3258+
em_expr = em->em_expr;
3259+
3260+
/*
3261+
* Lookup the operator corresponding to the strategy in the opclass.
3262+
* The datatype used by the opfamily is not necessarily the same as
3263+
* the expression type (for array types for example).
3264+
*/
3265+
oprid = get_opfamily_member(pathkey->pk_opfamily,
3266+
em->em_datatype,
3267+
em->em_datatype,
3268+
pathkey->pk_strategy);
3269+
if (!OidIsValid(oprid))
3270+
elog(ERROR, "missing operator %d(%u,%u) in opfamily %u",
3271+
pathkey->pk_strategy, em->em_datatype, em->em_datatype,
3272+
pathkey->pk_opfamily);
32013273

32023274
appendStringInfoString(buf, delim);
32033275
deparseExpr(em_expr, context);
3204-
if (pathkey->pk_strategy == BTLessStrategyNumber)
3205-
appendStringInfoString(buf, " ASC");
3206-
else
3207-
appendStringInfoString(buf, " DESC");
32083276

3209-
if (pathkey->pk_nulls_first)
3210-
appendStringInfoString(buf, " NULLS FIRST");
3211-
else
3212-
appendStringInfoString(buf, " NULLS LAST");
3277+
/*
3278+
* Here we need to use the expression's actual type to discover
3279+
* whether the desired operator will be the default or not.
3280+
*/
3281+
appendOrderBySuffix(oprid, exprType((Node *) em_expr),
3282+
pathkey->pk_nulls_first, context);
32133283

32143284
delim = ", ";
32153285
}

contrib/postgres_fdw/expected/postgres_fdw.out

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3265,6 +3265,19 @@ select array_agg(c1 order by c1 using operator(public.<^)) from ft2 where c2 = 6
32653265
Remote SQL: SELECT "C 1", c2 FROM "S 1"."T 1" WHERE (("C 1" < 100)) AND ((c2 = 6))
32663266
(6 rows)
32673267

3268+
-- This should not be pushed either.
3269+
explain (verbose, costs off)
3270+
select * from ft2 order by c1 using operator(public.<^);
3271+
QUERY PLAN
3272+
-------------------------------------------------------------------------------
3273+
Sort
3274+
Output: c1, c2, c3, c4, c5, c6, c7, c8
3275+
Sort Key: ft2.c1 USING <^
3276+
-> Foreign Scan on public.ft2
3277+
Output: c1, c2, c3, c4, c5, c6, c7, c8
3278+
Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1"
3279+
(6 rows)
3280+
32683281
-- Add into extension
32693282
alter extension postgres_fdw add operator class my_op_class using btree;
32703283
alter extension postgres_fdw add function my_op_cmp(a int, b int);
@@ -3290,6 +3303,16 @@ select array_agg(c1 order by c1 using operator(public.<^)) from ft2 where c2 = 6
32903303
{6,16,26,36,46,56,66,76,86,96}
32913304
(1 row)
32923305

3306+
-- This should be pushed too.
3307+
explain (verbose, costs off)
3308+
select * from ft2 order by c1 using operator(public.<^);
3309+
QUERY PLAN
3310+
-----------------------------------------------------------------------------------------------------------------------------
3311+
Foreign Scan on public.ft2
3312+
Output: c1, c2, c3, c4, c5, c6, c7, c8
3313+
Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" ORDER BY "C 1" USING OPERATOR(public.<^) NULLS LAST
3314+
(3 rows)
3315+
32933316
-- Remove from extension
32943317
alter extension postgres_fdw drop operator class my_op_class using btree;
32953318
alter extension postgres_fdw drop function my_op_cmp(a int, b int);

contrib/postgres_fdw/postgres_fdw.c

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#include "access/htup_details.h"
1818
#include "access/sysattr.h"
1919
#include "catalog/pg_class.h"
20+
#include "catalog/pg_opfamily.h"
2021
#include "commands/defrem.h"
2122
#include "commands/explain.h"
2223
#include "commands/vacuum.h"
@@ -825,22 +826,15 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
825826
foreach(lc, root->query_pathkeys)
826827
{
827828
PathKey *pathkey = (PathKey *) lfirst(lc);
828-
EquivalenceClass *pathkey_ec = pathkey->pk_eclass;
829-
Expr *em_expr;
830829

831830
/*
832831
* The planner and executor don't have any clever strategy for
833832
* taking data sorted by a prefix of the query's pathkeys and
834833
* getting it to be sorted by all of those pathkeys. We'll just
835834
* end up resorting the entire data set. So, unless we can push
836835
* down all of the query pathkeys, forget it.
837-
*
838-
* is_foreign_expr would detect volatile expressions as well, but
839-
* checking ec_has_volatile here saves some cycles.
840836
*/
841-
if (pathkey_ec->ec_has_volatile ||
842-
!(em_expr = find_em_expr_for_rel(pathkey_ec, rel)) ||
843-
!is_foreign_expr(root, rel, em_expr))
837+
if (!is_foreign_pathkey(root, rel, pathkey))
844838
{
845839
query_pathkeys_ok = false;
846840
break;
@@ -884,16 +878,19 @@ get_useful_pathkeys_for_relation(PlannerInfo *root, RelOptInfo *rel)
884878
foreach(lc, useful_eclass_list)
885879
{
886880
EquivalenceClass *cur_ec = lfirst(lc);
887-
Expr *em_expr;
888881
PathKey *pathkey;
889882

890883
/* If redundant with what we did above, skip it. */
891884
if (cur_ec == query_ec)
892885
continue;
893886

887+
/* Can't push down the sort if the EC's opfamily is not shippable. */
888+
if (!is_shippable(linitial_oid(cur_ec->ec_opfamilies),
889+
OperatorFamilyRelationId, fpinfo))
890+
continue;
891+
894892
/* If no pushable expression for this rel, skip it. */
895-
em_expr = find_em_expr_for_rel(cur_ec, rel);
896-
if (em_expr == NULL || !is_foreign_expr(root, rel, em_expr))
893+
if (find_em_for_rel(root, cur_ec, rel) == NULL)
897894
continue;
898895

899896
/* Looks like we can generate a pathkey, so let's do it. */
@@ -5916,30 +5913,35 @@ conversion_error_callback(void *arg)
59165913
}
59175914

59185915
/*
5919-
* Find an equivalence class member expression, all of whose Vars, come from
5920-
* the indicated relation.
5916+
* Given an EquivalenceClass and a foreign relation, find an EC member
5917+
* that can be used to sort the relation remotely according to a pathkey
5918+
* using this EC.
5919+
*
5920+
* If there is more than one suitable candidate, return an arbitrary
5921+
* one of them. If there is none, return NULL.
5922+
*
5923+
* This checks that the EC member expression uses only Vars from the given
5924+
* rel and is shippable. Caller must separately verify that the pathkey's
5925+
* ordering operator is shippable.
59215926
*/
5922-
Expr *
5923-
find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel)
5927+
EquivalenceMember *
5928+
find_em_for_rel(PlannerInfo *root, EquivalenceClass *ec, RelOptInfo *rel)
59245929
{
5925-
ListCell *lc_em;
5930+
ListCell *lc;
59265931

5927-
foreach(lc_em, ec->ec_members)
5932+
foreach(lc, ec->ec_members)
59285933
{
5929-
EquivalenceMember *em = lfirst(lc_em);
5934+
EquivalenceMember *em = (EquivalenceMember *) lfirst(lc);
59305935

5936+
/*
5937+
* Note we require !bms_is_empty, else we'd accept constant
5938+
* expressions which are not suitable for the purpose.
5939+
*/
59315940
if (bms_is_subset(em->em_relids, rel->relids) &&
5932-
!bms_is_empty(em->em_relids))
5933-
{
5934-
/*
5935-
* If there is more than one equivalence member whose Vars are
5936-
* taken entirely from this relation, we'll be content to choose
5937-
* any one of those.
5938-
*/
5939-
return em->em_expr;
5940-
}
5941+
!bms_is_empty(em->em_relids) &&
5942+
is_foreign_expr(root, rel, em->em_expr))
5943+
return em;
59415944
}
59425945

5943-
/* We didn't find any suitable equivalence class expression */
59445946
return NULL;
59455947
}

contrib/postgres_fdw/postgres_fdw.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ extern bool is_foreign_expr(PlannerInfo *root,
143143
extern bool is_foreign_param(PlannerInfo *root,
144144
RelOptInfo *baserel,
145145
Expr *expr);
146+
extern bool is_foreign_pathkey(PlannerInfo *root,
147+
RelOptInfo *baserel,
148+
PathKey *pathkey);
146149
extern void deparseInsertSql(StringInfo buf, RangeTblEntry *rte,
147150
Index rtindex, Relation rel,
148151
List *targetAttrs, bool doNothing, List *returningList,
@@ -175,7 +178,9 @@ extern void deparseAnalyzeSizeSql(StringInfo buf, Relation rel);
175178
extern void deparseAnalyzeSql(StringInfo buf, Relation rel,
176179
List **retrieved_attrs);
177180
extern void deparseStringLiteral(StringInfo buf, const char *val);
178-
extern Expr *find_em_expr_for_rel(EquivalenceClass *ec, RelOptInfo *rel);
181+
extern EquivalenceMember *find_em_for_rel(PlannerInfo *root,
182+
EquivalenceClass *ec,
183+
RelOptInfo *rel);
179184
extern List *build_tlist_to_deparse(RelOptInfo *foreignrel);
180185
extern void deparseSelectStmtForRel(StringInfo buf, PlannerInfo *root,
181186
RelOptInfo *foreignrel, List *tlist,

0 commit comments

Comments
 (0)