diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c index 81cb2b4..08bd352 100644 --- a/contrib/postgres_fdw/deparse.c +++ b/contrib/postgres_fdw/deparse.c @@ -44,8 +44,11 @@ #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "commands/defrem.h" +#include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "nodes/plannodes.h" #include "optimizer/clauses.h" +#include "optimizer/prep.h" #include "optimizer/var.h" #include "parser/parsetree.h" #include "utils/builtins.h" @@ -89,6 +92,8 @@ typedef struct deparse_expr_cxt RelOptInfo *foreignrel; /* the foreign relation we are planning for */ StringInfo buf; /* output buffer to append to */ List **params_list; /* exprs that will become remote Params */ + List *outertlist; /* outer child's target list */ + List *innertlist; /* inner child's target list */ } deparse_expr_cxt; /* @@ -108,7 +113,8 @@ static void deparseTargetList(StringInfo buf, Index rtindex, Relation rel, Bitmapset *attrs_used, - List **retrieved_attrs); + List **retrieved_attrs, + bool alias); static void deparseReturningList(StringInfo buf, PlannerInfo *root, Index rtindex, Relation rel, bool trig_after_row, @@ -136,6 +142,7 @@ static void printRemoteParam(int paramindex, Oid paramtype, int32 paramtypmod, deparse_expr_cxt *context); static void printRemotePlaceholder(Oid paramtype, int32 paramtypmod, deparse_expr_cxt *context); +static const char *get_jointype_name(JoinType jointype); /* @@ -250,7 +257,7 @@ foreign_expr_walker(Node *node, * Param's collation, ie it's not safe for it to have a * non-default collation. */ - if (var->varno == glob_cxt->foreignrel->relid && + if (bms_is_member(var->varno, glob_cxt->foreignrel->relids) && var->varlevelsup == 0) { /* Var belongs to foreign table */ @@ -675,18 +682,88 @@ is_builtin(Oid oid) * * We also create an integer List of the columns being retrieved, which is * returned to *retrieved_attrs. + * + * The relations is a string buffer for "Relations" portion of EXPLAIN output, + * or NULL if caller doesn't need it. Note that it should have been + * initialized by caller. + * + * The alias is a flag to add aliases of columns and tables. This should be + * false in the initial call, and will be set true when this function is called + * for building a part of a join query. */ void deparseSelectSql(StringInfo buf, PlannerInfo *root, RelOptInfo *baserel, Bitmapset *attrs_used, - List **retrieved_attrs) + List *remote_conds, + List **params_list, + List **fdw_scan_tlist, + List **retrieved_attrs, + StringInfo relations, + bool alias) { + PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) baserel->fdw_private; RangeTblEntry *rte = planner_rt_fetch(baserel->relid, root); Relation rel; /* + * If given relation was a join relation, recursively construct statement + * by putting each outer and inner relations in FROM clause as a subquery + * with aliasing. + */ + if (baserel->reloptkind == RELOPT_JOINREL) + { + RelOptInfo *rel_o = fpinfo->outerrel; + RelOptInfo *rel_i = fpinfo->innerrel; + PgFdwRelationInfo *fpinfo_o = (PgFdwRelationInfo *) rel_o->fdw_private; + PgFdwRelationInfo *fpinfo_i = (PgFdwRelationInfo *) rel_i->fdw_private; + StringInfoData sql_o; + StringInfoData sql_i; + List *ret_attrs_tmp; /* not used */ + StringInfoData relations_o; + StringInfoData relations_i; + const char *jointype_str; + + /* + * Deparse query for outer and inner relation, and combine them into + * a query. + * + * Here we don't pass fdw_scan_tlist because targets of underlying + * relations are already put in joinrel->reltargetlist, and + * deparseJoinRel() takes all care about it. + */ + initStringInfo(&sql_o); + initStringInfo(&relations_o); + deparseSelectSql(&sql_o, root, rel_o, fpinfo_o->attrs_used, + fpinfo_o->remote_conds, params_list, + NULL, &ret_attrs_tmp, &relations_o, true); + initStringInfo(&sql_i); + initStringInfo(&relations_i); + deparseSelectSql(&sql_i, root, rel_i, fpinfo_i->attrs_used, + fpinfo_i->remote_conds, params_list, + NULL, &ret_attrs_tmp, &relations_i, true); + + /* For EXPLAIN output */ + jointype_str = get_jointype_name(fpinfo->jointype); + if (relations) + appendStringInfo(relations, "(%s) %s JOIN (%s)", + relations_o.data, jointype_str, relations_i.data); + + deparseJoinSql(buf, root, baserel, + fpinfo->outerrel, + fpinfo->innerrel, + sql_o.data, + sql_i.data, + fpinfo->jointype, + fpinfo->joinclauses, + fpinfo->otherclauses, + fdw_scan_tlist, + retrieved_attrs); + return; + } + + /* * Core code already has some lock on each rel being planned, so we can * use NoLock here. */ @@ -697,7 +774,7 @@ deparseSelectSql(StringInfo buf, */ appendStringInfoString(buf, "SELECT "); deparseTargetList(buf, root, baserel->relid, rel, attrs_used, - retrieved_attrs); + retrieved_attrs, alias); /* * Construct FROM clause @@ -705,6 +782,87 @@ deparseSelectSql(StringInfo buf, appendStringInfoString(buf, " FROM "); deparseRelation(buf, rel); + /* + * Return local relation name for EXPLAIN output. + * We can't know VERBOSE option is specified or not, so always add shcema + * name. + */ + if (relations) + { + const char *namespace; + const char *relname; + const char *refname; + + namespace = get_namespace_name(get_rel_namespace(rte->relid)); + relname = get_rel_name(rte->relid); + refname = rte->eref->aliasname; + appendStringInfo(relations, "%s.%s", + quote_identifier(namespace), + quote_identifier(relname)); + if (*refname && strcmp(refname, relname) != 0) + appendStringInfo(relations, " %s", + quote_identifier(rte->eref->aliasname)); + } + + /* + * Construct WHERE clause + */ + if (remote_conds) + appendConditions(buf, root, baserel, NULL, NULL, remote_conds, + " WHERE ", params_list); + + /* + * Add FOR UPDATE/SHARE if appropriate. We apply locking during the + * initial row fetch, rather than later on as is done for local tables. + * The extra roundtrips involved in trying to duplicate the local + * semantics exactly don't seem worthwhile (see also comments for + * RowMarkType). + * + * Note: because we actually run the query as a cursor, this assumes + * that DECLARE CURSOR ... FOR UPDATE is supported, which it isn't + * before 8.3. + */ + if (baserel->relid == root->parse->resultRelation && + (root->parse->commandType == CMD_UPDATE || + root->parse->commandType == CMD_DELETE)) + { + /* Relation is UPDATE/DELETE target, so use FOR UPDATE */ + appendStringInfoString(buf, " FOR UPDATE"); + } + else + { + PlanRowMark *rc = get_plan_rowmark(root->rowMarks, baserel->relid); + + if (rc) + { + /* + * Relation is specified as a FOR UPDATE/SHARE target, so handle + * that. (But we could also see LCS_NONE, meaning this isn't a + * target relation after all.) + * + * For now, just ignore any [NO] KEY specification, since (a) + * it's not clear what that means for a remote table that we + * don't have complete information about, and (b) it wouldn't + * work anyway on older remote servers. Likewise, we don't + * worry about NOWAIT. + */ + switch (rc->strength) + { + case LCS_NONE: + /* No locking needed */ + break; + case LCS_FORKEYSHARE: + case LCS_FORSHARE: + appendStringInfoString(buf, " FOR SHARE"); + break; + case LCS_FORNOKEYUPDATE: + case LCS_FORUPDATE: + appendStringInfoString(buf, " FOR UPDATE"); + break; + } + } + } + heap_close(rel, NoLock); } @@ -721,7 +879,8 @@ deparseTargetList(StringInfo buf, Index rtindex, Relation rel, Bitmapset *attrs_used, - List **retrieved_attrs) + List **retrieved_attrs, + bool alias) { TupleDesc tupdesc = RelationGetDescr(rel); bool have_wholerow; @@ -752,6 +911,9 @@ deparseTargetList(StringInfo buf, first = false; deparseColumnRef(buf, rtindex, i, root); + if (alias) + appendStringInfo(buf, " a%d", + i - FirstLowInvalidHeapAttributeNumber); *retrieved_attrs = lappend_int(*retrieved_attrs, i); } @@ -769,6 +931,9 @@ deparseTargetList(StringInfo buf, first = false; appendStringInfoString(buf, "ctid"); + if (alias) + appendStringInfo(buf, " a%d", + SelfItemPointerAttributeNumber - FirstLowInvalidHeapAttributeNumber); *retrieved_attrs = lappend_int(*retrieved_attrs, SelfItemPointerAttributeNumber); @@ -780,11 +945,13 @@ deparseTargetList(StringInfo buf, } /* - * Deparse WHERE clauses in given list of RestrictInfos and append them to buf. + * Deparse conditions, such as WHERE clause and ON clause of JOIN, in the given + * list, consist of RestrictInfo or Expr, and append string representation of + * them to buf. * * baserel is the foreign table we're planning for. * - * If no WHERE clause already exists in the buffer, is_first should be true. + * prefix is placed before the conditions, if any. * * If params is not NULL, it receives a list of Params and other-relation Vars * used in the clauses; these values must be transmitted to the remote server @@ -794,16 +961,19 @@ deparseTargetList(StringInfo buf, * so Params and other-relation Vars should be replaced by dummy values. */ void -appendWhereClause(StringInfo buf, - PlannerInfo *root, - RelOptInfo *baserel, - List *exprs, - bool is_first, - List **params) +appendConditions(StringInfo buf, + PlannerInfo *root, + RelOptInfo *baserel, + List *outertlist, + List *innertlist, + List *exprs, + const char *prefix, + List **params) { deparse_expr_cxt context; int nestlevel; ListCell *lc; + bool is_first = prefix == NULL ? false : true; if (params) *params = NIL; /* initialize result list to empty */ @@ -813,22 +983,36 @@ appendWhereClause(StringInfo buf, context.foreignrel = baserel; context.buf = buf; context.params_list = params; + context.outertlist = outertlist; + context.innertlist = innertlist; /* Make sure any constants in the exprs are printed portably */ nestlevel = set_transmission_modes(); foreach(lc, exprs) { + Node *node = (Node *) lfirst(lc); RestrictInfo *ri = (RestrictInfo *) lfirst(lc); + Expr *expr = (Expr *) lfirst(lc); + + if (IsA(node, RestrictInfo)) + { + expr = ri->clause; + } + else + { + expr = ri->clause; + expr = (Expr *) node; + } /* Connect expressions with "AND" and parenthesize each condition. */ if (is_first) - appendStringInfoString(buf, " WHERE "); + appendStringInfoString(buf, prefix); else appendStringInfoString(buf, " AND "); appendStringInfoChar(buf, '('); - deparseExpr(ri->clause, &context); + deparseExpr(expr, &context); appendStringInfoChar(buf, ')'); is_first = false; @@ -838,6 +1022,297 @@ appendWhereClause(StringInfo buf, } /* + * Returns position index (start with 1) of given var in given target list, or + * 0 when not found. + */ +static int +find_var_pos(Var *node, List *tlist) +{ + int pos = 1; + ListCell *lc; + + foreach(lc, tlist) + { + Var *var = (Var *) lfirst(lc); + + if (equal(var, node)) + { + return pos; + } + pos++; + } + + return 0; +} + +/* + * Deparse given Var into buf. + */ +static void +deparseJoinVar(Var *node, deparse_expr_cxt *context) +{ + char side; + int pos; + + pos = find_var_pos(node, context->outertlist); + if (pos > 0) + side = 'l'; + else + { + side = 'r'; + pos = find_var_pos(node, context->innertlist); + } + + /* + * We treat whole-row reference same as ordinary attribute references, + * because such transformation should be done in lower level. + */ + appendStringInfo(context->buf, "%c.a%d", side, pos); +} + +/* + * Deparse column alias list for a subquery in FROM clause. + */ +static void +deparseColumnAliases(StringInfo buf, List *tlist) +{ + int pos; + ListCell *lc; + + pos = 1; + foreach(lc, tlist) + { + /* Deparse column alias for the subquery */ + if (pos > 1) + appendStringInfoString(buf, ", "); + appendStringInfo(buf, "a%d", pos); + pos++; + } +} + +/* + * Deparse "wrapper" SQL for a query which projects target lists in proper + * order and contents. Note that this treatment is necessary only for queries + * used in FROM clause of a join query. + * + * Even if the SQL is enough simple (no ctid, no whole-row reference), the order + * of output column might different from underlying scan, so we always need to + * wrap the queries for join sources. + * + */ +static const char * +deparseProjectionSql(PlannerInfo *root, + RelOptInfo *baserel, + const char *sql, + char side) +{ + StringInfoData wholerow; + StringInfoData buf; + ListCell *lc; + bool first; + bool have_wholerow = false; + + /* + * We have nothing to do if the targetlist contains no special reference, + * such as whole-row and ctid. + */ + foreach(lc, baserel->reltargetlist) + { + Var *var = (Var *) lfirst(lc); + if (var->varattno == 0) + { + have_wholerow = true; + break; + } + } + + /* + * Construct whole-row reference with ROW() syntax + */ + if (have_wholerow) + { + RangeTblEntry *rte; + Relation rel; + TupleDesc tupdesc; + int i; + + /* Obtain TupleDesc for deparsing all valid columns */ + rte = planner_rt_fetch(baserel->relid, root); + rel = heap_open(rte->relid, NoLock); + tupdesc = rel->rd_att; + + /* Print all valid columns in ROW() to generate whole-row value */ + initStringInfo(&wholerow); + appendStringInfoString(&wholerow, "ROW("); + first = true; + for (i = 1; i <= tupdesc->natts; i++) + { + Form_pg_attribute attr = tupdesc->attrs[i - 1]; + + /* Ignore dropped columns. */ + if (attr->attisdropped) + continue; + + if (!first) + appendStringInfoString(&wholerow, ", "); + first = false; + + appendStringInfo(&wholerow, "%c.a%d", side, + i - FirstLowInvalidHeapAttributeNumber); + } + appendStringInfoString(&wholerow, ")"); + + heap_close(rel, NoLock); + } + + /* + * Construct a SELECT statement which has the original query in its FROM + * clause, and have target list entries in its SELECT clause. The number + * used in column aliases are attnum - FirstLowInvalidHeapAttributeNumber in + * order to make all numbers positive even for system columns which have + * minus value as attnum. + */ + initStringInfo(&buf); + appendStringInfoString(&buf, "SELECT "); + first = true; + foreach(lc, baserel->reltargetlist) + { + Var *var = (Var *) lfirst(lc); + + if (!first) + appendStringInfoString(&buf, ", "); + + if (var->varattno == 0) + appendStringInfo(&buf, "%s", wholerow.data); + else + appendStringInfo(&buf, "%c.a%d", side, + var->varattno - FirstLowInvalidHeapAttributeNumber); + + first = false; + } + appendStringInfo(&buf, " FROM (%s) %c", sql, side); + + return buf.data; +} + +static const char * +get_jointype_name(JoinType jointype) +{ + if (jointype == JOIN_INNER) + return "INNER"; + else if (jointype == JOIN_LEFT) + return "LEFT"; + else if (jointype == JOIN_RIGHT) + return "RIGHT"; + else if (jointype == JOIN_FULL) + return "FULL"; + + /* not reached */ + elog(ERROR, "unsupported join type %d", jointype); +} + +/* + * Construct a SELECT statement which contains join clause. + * + * We also create an TargetEntry List of the columns being retrieved, which is + * returned to *fdw_scan_tlist. + * + * path_o, tl_o, sql_o are respectively path, targetlist, and remote query + * statement of the outer child relation. postfix _i means those for the inner + * child relation. jointype and joinclauses are information of join method. + * fdw_scan_tlist is output parameter to pass target list of the pseudo scan to + * caller. + */ +void +deparseJoinSql(StringInfo buf, + PlannerInfo *root, + RelOptInfo *baserel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + const char *sql_o, + const char *sql_i, + JoinType jointype, + List *joinclauses, + List *otherclauses, + List **fdw_scan_tlist, + List **retrieved_attrs) +{ + StringInfoData selbuf; /* buffer for SELECT clause */ + StringInfoData abuf_o; /* buffer for column alias list of outer */ + StringInfoData abuf_i; /* buffer for column alias list of inner */ + int i; + ListCell *lc; + const char *jointype_str; + deparse_expr_cxt context; + + context.root = root; + context.foreignrel = baserel; + context.buf = &selbuf; + context.params_list = NULL; + context.outertlist = outerrel->reltargetlist; + context.innertlist = innerrel->reltargetlist; + + jointype_str = get_jointype_name(jointype); + *retrieved_attrs = NIL; + + /* print SELECT clause of the join scan */ + initStringInfo(&selbuf); + i = 0; + foreach(lc, baserel->reltargetlist) + { + Var *var = (Var *) lfirst(lc); + TargetEntry *tle; + + if (i > 0) + appendStringInfoString(&selbuf, ", "); + deparseJoinVar(var, &context); + + tle = makeTargetEntry((Expr *) var, i + 1, NULL, false); + if (fdw_scan_tlist) + *fdw_scan_tlist = lappend(*fdw_scan_tlist, tle); + + *retrieved_attrs = lappend_int(*retrieved_attrs, i + 1); + + i++; + } + if (i == 0) + appendStringInfoString(&selbuf, "NULL"); + + /* + * Do pseudo-projection for an underlying scan on a foreign table, if a) the + * relation is a base relation, and b) its targetlist contains whole-row + * reference. + */ + if (outerrel->reloptkind == RELOPT_BASEREL) + sql_o = deparseProjectionSql(root, outerrel, sql_o, 'l'); + if (innerrel->reloptkind == RELOPT_BASEREL) + sql_i = deparseProjectionSql(root, innerrel, sql_i, 'r'); + + /* Deparse column alias portion of subquery in FROM clause. */ + initStringInfo(&abuf_o); + deparseColumnAliases(&abuf_o, outerrel->reltargetlist); + initStringInfo(&abuf_i); + deparseColumnAliases(&abuf_i, innerrel->reltargetlist); + + /* Construct SELECT statement */ + appendStringInfo(buf, "SELECT %s FROM", selbuf.data); + appendStringInfo(buf, " (%s) l (%s) %s JOIN (%s) r (%s)", + sql_o, abuf_o.data, jointype_str, sql_i, abuf_i.data); + /* Append ON clause */ + if (joinclauses) + appendConditions(buf, root, baserel, + outerrel->reltargetlist, innerrel->reltargetlist, + joinclauses, + " ON ", NULL); + /* Append WHERE clause */ + if (otherclauses) + appendConditions(buf, root, baserel, + outerrel->reltargetlist, innerrel->reltargetlist, + otherclauses, + " WHERE ", NULL); +} + +/* * deparse remote INSERT statement * * The statement text is appended to buf, and we also create an integer List @@ -997,7 +1472,7 @@ deparseReturningList(StringInfo buf, PlannerInfo *root, { appendStringInfoString(buf, " RETURNING "); deparseTargetList(buf, root, rtindex, rel, attrs_used, - retrieved_attrs); + retrieved_attrs, false); } else *retrieved_attrs = NIL; @@ -1264,6 +1739,8 @@ deparseExpr(Expr *node, deparse_expr_cxt *context) /* * Deparse given Var node into context->buf. * + * If context has valid innerrel, this is invoked for a join conditions. + * * If the Var belongs to the foreign relation, just print its remote name. * Otherwise, it's effectively a Param (and will in fact be a Param at * run time). Handle it the same way we handle plain Params --- see @@ -1274,39 +1751,46 @@ deparseVar(Var *node, deparse_expr_cxt *context) { StringInfo buf = context->buf; - if (node->varno == context->foreignrel->relid && - node->varlevelsup == 0) + if (context->foreignrel->reloptkind == RELOPT_JOINREL) { - /* Var belongs to foreign table */ - deparseColumnRef(buf, node->varno, node->varattno, context->root); + deparseJoinVar(node, context); } else { - /* Treat like a Param */ - if (context->params_list) + if (node->varno == context->foreignrel->relid && + node->varlevelsup == 0) { - int pindex = 0; - ListCell *lc; - - /* find its index in params_list */ - foreach(lc, *context->params_list) + /* Var belongs to foreign table */ + deparseColumnRef(buf, node->varno, node->varattno, context->root); + } + else + { + /* Treat like a Param */ + if (context->params_list) { - pindex++; - if (equal(node, (Node *) lfirst(lc))) - break; + int pindex = 0; + ListCell *lc; + + /* find its index in params_list */ + foreach(lc, *context->params_list) + { + pindex++; + if (equal(node, (Node *) lfirst(lc))) + break; + } + if (lc == NULL) + { + /* not in list, so add it */ + pindex++; + *context->params_list = lappend(*context->params_list, node); + } + + printRemoteParam(pindex, node->vartype, node->vartypmod, context); } - if (lc == NULL) + else { - /* not in list, so add it */ - pindex++; - *context->params_list = lappend(*context->params_list, node); + printRemotePlaceholder(node->vartype, node->vartypmod, context); } - - printRemoteParam(pindex, node->vartype, node->vartypmod, context); - } - else - { - printRemotePlaceholder(node->vartype, node->vartypmod, context); } } } diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index 1f417b3..80e22ae 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -9,11 +9,16 @@ DO $d$ OPTIONS (dbname '$$||current_database()||$$', port '$$||current_setting('port')||$$' )$$; + EXECUTE $$CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '$$||current_database()||$$', + port '$$||current_setting('port')||$$' + )$$; END; $d$; CREATE USER MAPPING FOR public SERVER testserver1 OPTIONS (user 'value', password 'value'); CREATE USER MAPPING FOR CURRENT_USER SERVER loopback; +CREATE USER MAPPING FOR CURRENT_USER SERVER loopback2; -- =================================================================== -- create objects used through FDW loopback server -- =================================================================== @@ -35,6 +40,18 @@ CREATE TABLE "S 1"."T 2" ( c2 text, CONSTRAINT t2_pkey PRIMARY KEY (c1) ); +CREATE TABLE "S 1"."T 3" ( + c1 int NOT NULL, + c2 int NOT NULL, + c3 text, + CONSTRAINT t3_pkey PRIMARY KEY (c1) +); +CREATE TABLE "S 1"."T 4" ( + c1 int NOT NULL, + c2 int NOT NULL, + c4 text, + CONSTRAINT t4_pkey PRIMARY KEY (c1) +); INSERT INTO "S 1"."T 1" SELECT id, id % 10, @@ -49,8 +66,22 @@ INSERT INTO "S 1"."T 2" SELECT id, 'AAA' || to_char(id, 'FM000') FROM generate_series(1, 100) id; +INSERT INTO "S 1"."T 3" + SELECT id, + id + 1, + 'AAA' || to_char(id, 'FM000') + FROM generate_series(1, 100) id; +DELETE FROM "S 1"."T 3" WHERE c1 % 2 != 0; -- delete for outer join tests +INSERT INTO "S 1"."T 4" + SELECT id, + id + 1, + 'AAA' || to_char(id, 'FM000') + FROM generate_series(1, 100) id; +DELETE FROM "S 1"."T 4" WHERE c1 % 3 != 0; -- delete for outer join tests ANALYZE "S 1"."T 1"; ANALYZE "S 1"."T 2"; +ANALYZE "S 1"."T 3"; +ANALYZE "S 1"."T 4"; -- =================================================================== -- create foreign tables -- =================================================================== @@ -78,6 +109,26 @@ CREATE FOREIGN TABLE ft2 ( c8 user_enum ) SERVER loopback; ALTER FOREIGN TABLE ft2 DROP COLUMN cx; +CREATE FOREIGN TABLE ft4 ( + c1 int NOT NULL, + c2 int NOT NULL, + c3 text +) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 3'); +CREATE FOREIGN TABLE ft5 ( + c1 int NOT NULL, + c2 int NOT NULL, + c3 text +) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 4'); +CREATE FOREIGN TABLE ft6 ( + c1 int NOT NULL, + c2 int NOT NULL, + c3 text +) SERVER loopback2 OPTIONS (schema_name 'S 1', table_name 'T 4'); +CREATE USER view_owner; +GRANT ALL ON ft5 TO view_owner; +CREATE VIEW v_ft5 AS SELECT * FROM ft5; +ALTER VIEW v_ft5 OWNER TO view_owner; +CREATE USER MAPPING FOR view_owner SERVER loopback; -- =================================================================== -- tests for validator -- =================================================================== @@ -119,12 +170,15 @@ ALTER FOREIGN TABLE ft2 OPTIONS (schema_name 'S 1', table_name 'T 1'); ALTER FOREIGN TABLE ft1 ALTER COLUMN c1 OPTIONS (column_name 'C 1'); ALTER FOREIGN TABLE ft2 ALTER COLUMN c1 OPTIONS (column_name 'C 1'); \det+ - List of foreign tables - Schema | Table | Server | FDW Options | Description ---------+-------+----------+---------------------------------------+------------- - public | ft1 | loopback | (schema_name 'S 1', table_name 'T 1') | - public | ft2 | loopback | (schema_name 'S 1', table_name 'T 1') | -(2 rows) + List of foreign tables + Schema | Table | Server | FDW Options | Description +--------+-------+-----------+---------------------------------------+------------- + public | ft1 | loopback | (schema_name 'S 1', table_name 'T 1') | + public | ft2 | loopback | (schema_name 'S 1', table_name 'T 1') | + public | ft4 | loopback | (schema_name 'S 1', table_name 'T 3') | + public | ft5 | loopback | (schema_name 'S 1', table_name 'T 4') | + public | ft6 | loopback2 | (schema_name 'S 1', table_name 'T 4') | +(5 rows) -- Now we should be able to run ANALYZE. -- To exercise multiple code paths, we use local stats on ft1 @@ -277,22 +331,6 @@ SELECT COUNT(*) FROM ft1 t1; 1000 (1 row) --- join two tables -SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; - c1 ------ - 101 - 102 - 103 - 104 - 105 - 106 - 107 - 108 - 109 - 110 -(10 rows) - -- subquery SELECT * FROM ft1 t1 WHERE t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 <= 10) ORDER BY c1; c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 @@ -489,17 +527,13 @@ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE c8 = 'foo'; -- can't -- parameterized remote path EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft2 a, ft2 b WHERE a.c1 = 47 AND b.c1 = a.c2; - QUERY PLAN -------------------------------------------------------------------------------------------------------------- - Nested Loop + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Foreign Scan Output: a.c1, a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8, b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8 - -> Foreign Scan on public.ft2 a - Output: a.c1, a.c2, a.c3, a.c4, a.c5, a.c6, a.c7, a.c8 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (("C 1" = 47)) - -> Foreign Scan on public.ft2 b - Output: b.c1, b.c2, b.c3, b.c4, b.c5, b.c6, b.c7, b.c8 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE (($1::integer = "C 1")) -(8 rows) + Relations: (public.ft2 a) INNER JOIN (public.ft2 b) + Remote SQL: SELECT l.a1, l.a2, l.a3, l.a4, l.a5, l.a6, l.a7, l.a8, r.a1, r.a2, r.a3, r.a4, r.a5, r.a6, r.a7, r.a8 FROM (SELECT l.a9, l.a10, l.a12, l.a13, l.a14, l.a15, l.a16, l.a17 FROM (SELECT "C 1" a9, c2 a10, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1" WHERE (("C 1" = 47))) l) l (a1, a2, a3, a4, a5, a6, a7, a8) INNER JOIN (SELECT r.a9, r.a10, r.a12, r.a13, r.a14, r.a15, r.a16, r.a17 FROM (SELECT "C 1" a9, c2 a10, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1") r) r (a1, a2, a3, a4, a5, a6, a7, a8) ON ((l.a2 = r.a1)) +(4 rows) SELECT * FROM ft2 a, ft2 b WHERE a.c1 = 47 AND b.c1 = a.c2; c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 @@ -651,6 +685,670 @@ SELECT * FROM ft2 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft1 WHERE c1 < 5)); (4 rows) -- =================================================================== +-- JOIN queries +-- =================================================================== +-- join two tables +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1, t1.c3 + -> Sort + Output: t1.c1, t2.c1, t1.c3 + Sort Key: t1.c3, t1.c1 + -> Foreign Scan + Output: t1.c1, t2.c1, t1.c3 + Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) + Remote SQL: SELECT l.a1, l.a2, r.a1 FROM (SELECT l.a10, l.a12 FROM (SELECT "C 1" a10, c3 a12 FROM "S 1"."T 1") l) l (a1, a2) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1)) +(9 rows) + +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + c1 | c1 +-----+----- + 101 | 101 + 102 | 102 + 103 | 103 + 104 | 104 + 105 | 105 + 106 | 106 + 107 | 107 + 108 | 108 + 109 | 109 + 110 | 110 +(10 rows) + +-- join three tables +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c2, t3.c3, t1.c3 + -> Sort + Output: t1.c1, t2.c2, t3.c3, t1.c3 + Sort Key: t1.c3, t1.c1 + -> Foreign Scan + Output: t1.c1, t2.c2, t3.c3, t1.c3 + Relations: ((public.ft1 t1) INNER JOIN (public.ft2 t2)) INNER JOIN (public.ft4 t3) + Remote SQL: SELECT l.a1, l.a2, l.a3, r.a1 FROM (SELECT l.a1, l.a2, r.a1, r.a2 FROM (SELECT l.a10, l.a12 FROM (SELECT "C 1" a10, c3 a12 FROM "S 1"."T 1") l) l (a1, a2) INNER JOIN (SELECT r.a10, r.a9 FROM (SELECT "C 1" a9, c2 a10 FROM "S 1"."T 1") r) r (a1, a2) ON ((l.a1 = r.a2))) l (a1, a2, a3, a4) INNER JOIN (SELECT r.a11, r.a9 FROM (SELECT c1 a9, c3 a11 FROM "S 1"."T 3") r) r (a1, a2) ON ((l.a1 = r.a2)) +(9 rows) + +SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10; + c1 | c2 | c3 +----+----+-------- + 22 | 2 | AAA022 + 24 | 4 | AAA024 + 26 | 6 | AAA026 + 28 | 8 | AAA028 + 30 | 0 | AAA030 + 32 | 2 | AAA032 + 34 | 4 | AAA034 + 36 | 6 | AAA036 + 38 | 8 | AAA038 + 40 | 0 | AAA040 +(10 rows) + +-- left outer join +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1 + -> Sort + Output: t1.c1, t2.c1 + Sort Key: t1.c1, t2.c1 + -> Foreign Scan + Output: t1.c1, t2.c1 + Relations: (public.ft4 t1) LEFT JOIN (public.ft5 t2) + Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") l) l (a1) LEFT JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 4") r) r (a1) ON ((l.a1 = r.a1)) +(9 rows) + +SELECT t1.c1, t2.c1 FROM ft4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; + c1 | c1 +----+---- + 22 | + 24 | 24 + 26 | + 28 | + 30 | 30 + 32 | + 34 | + 36 | 36 + 38 | + 40 | +(10 rows) + +-- right outer join +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft5 t1 RIGHT JOIN ft4 t2 ON (t1.c1 = t2.c1) ORDER BY t2.c1, t1.c1 OFFSET 10 LIMIT 10; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1 + -> Sort + Output: t1.c1, t2.c1 + Sort Key: t2.c1, t1.c1 + -> Foreign Scan + Output: t1.c1, t2.c1 + Relations: (public.ft4 t2) LEFT JOIN (public.ft5 t1) + Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") l) l (a1) LEFT JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 4") r) r (a1) ON ((r.a1 = l.a1)) +(9 rows) + +SELECT t1.c1, t2.c1 FROM ft5 t1 RIGHT JOIN ft4 t2 ON (t1.c1 = t2.c1) ORDER BY t2.c1, t1.c1 OFFSET 10 LIMIT 10; + c1 | c1 +----+---- + | 22 + 24 | 24 + | 26 + | 28 + 30 | 30 + | 32 + | 34 + 36 | 36 + | 38 + | 40 +(10 rows) + +-- full outer join +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1 + -> Sort + Output: t1.c1, t2.c1 + Sort Key: t1.c1, t2.c1 + -> Foreign Scan + Output: t1.c1, t2.c1 + Relations: (public.ft4 t1) FULL JOIN (public.ft5 t2) + Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") l) l (a1) FULL JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 4") r) r (a1) ON ((l.a1 = r.a1)) +(9 rows) + +SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10; + c1 | c1 +-----+---- + 92 | + 94 | + 96 | 96 + 98 | + 100 | + | 3 + | 9 + | 15 + | 21 + | 27 +(10 rows) + +-- full outer join + WHERE clause, only matched rows +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1 + -> Sort + Output: t1.c1, t2.c1 + Sort Key: t1.c1, t2.c1 + -> Foreign Scan + Output: t1.c1, t2.c1 + Relations: (public.ft4 t1) FULL JOIN (public.ft5 t2) + Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") l) l (a1) FULL JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 4") r) r (a1) ON ((l.a1 = r.a1)) WHERE (((l.a1 = r.a1) OR (l.a1 IS NULL))) +(9 rows) + +SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; + c1 | c1 +----+---- + 66 | 66 + 72 | 72 + 78 | 78 + 84 | 84 + 90 | 90 + 96 | 96 + | 3 + | 9 + | 15 + | 21 +(10 rows) + +-- join at WHERE clause +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON true WHERE (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1, t1.c3 + -> Sort + Output: t1.c1, t2.c1, t1.c3 + Sort Key: t1.c3, t1.c1 + -> Foreign Scan + Output: t1.c1, t2.c1, t1.c3 + Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) + Remote SQL: SELECT l.a1, l.a2, r.a1 FROM (SELECT l.a10, l.a12 FROM (SELECT "C 1" a10, c3 a12 FROM "S 1"."T 1") l) l (a1, a2) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1)) +(9 rows) + +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON true WHERE (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + c1 | c1 +-----+----- + 101 | 101 + 102 | 102 + 103 | 103 + 104 | 104 + 105 | 105 + 106 | 106 + 107 | 107 + 108 | 108 + 109 | 109 + 110 | 110 +(10 rows) + +-- join in CTE +EXPLAIN (COSTS false, VERBOSE) +WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t.c1_1, t.c2_1, t.c1_3 + CTE t + -> Foreign Scan + Output: t1.c1, t1.c3, t2.c1 + Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) + Remote SQL: SELECT l.a1, l.a2, r.a1 FROM (SELECT l.a10, l.a12 FROM (SELECT "C 1" a10, c3 a12 FROM "S 1"."T 1") l) l (a1, a2) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1)) + -> Sort + Output: t.c1_1, t.c2_1, t.c1_3 + Sort Key: t.c1_3, t.c1_1 + -> CTE Scan on t + Output: t.c1_1, t.c2_1, t.c1_3 +(12 rows) + +WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10; + c1_1 | c2_1 +------+------ + 101 | 101 + 102 | 102 + 103 | 103 + 104 | 104 + 105 | 105 + 106 | 106 + 107 | 107 + 108 | 108 + 109 | 109 + 110 | 110 +(10 rows) + +-- ctid with whole-row reference +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3 + -> Sort + Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3 + Sort Key: t1.c3, t1.c1 + -> Foreign Scan + Output: t1.ctid, t1.*, t2.*, t1.c1, t1.c3 + Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) + Remote SQL: SELECT l.a1, l.a2, l.a3, l.a4, r.a1 FROM (SELECT l.a7, ROW(l.a10, l.a11, l.a12, l.a13, l.a14, l.a15, l.a16, l.a17), l.a10, l.a12 FROM (SELECT "C 1" a10, c2 a11, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17, ctid a7 FROM "S 1"."T 1") l) l (a1, a2, a3, a4) INNER JOIN (SELECT ROW(r.a9, r.a10, r.a12, r.a13, r.a14, r.a15, r.a16, r.a17), r.a9 FROM (SELECT "C 1" a9, c2 a10, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1") r) r (a1, a2) ON ((l.a3 = r.a2)) +(9 rows) + +SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + ctid | t1 | t2 | c1 +--------+--------------------------------------------------------------------------------------------+--------------------------------------------------------------------------------------------+----- + (1,4) | (101,1,00101,"Fri Jan 02 00:00:00 1970 PST","Fri Jan 02 00:00:00 1970",1,"1 ",foo) | (101,1,00101,"Fri Jan 02 00:00:00 1970 PST","Fri Jan 02 00:00:00 1970",1,"1 ",foo) | 101 + (1,5) | (102,2,00102,"Sat Jan 03 00:00:00 1970 PST","Sat Jan 03 00:00:00 1970",2,"2 ",foo) | (102,2,00102,"Sat Jan 03 00:00:00 1970 PST","Sat Jan 03 00:00:00 1970",2,"2 ",foo) | 102 + (1,6) | (103,3,00103,"Sun Jan 04 00:00:00 1970 PST","Sun Jan 04 00:00:00 1970",3,"3 ",foo) | (103,3,00103,"Sun Jan 04 00:00:00 1970 PST","Sun Jan 04 00:00:00 1970",3,"3 ",foo) | 103 + (1,7) | (104,4,00104,"Mon Jan 05 00:00:00 1970 PST","Mon Jan 05 00:00:00 1970",4,"4 ",foo) | (104,4,00104,"Mon Jan 05 00:00:00 1970 PST","Mon Jan 05 00:00:00 1970",4,"4 ",foo) | 104 + (1,8) | (105,5,00105,"Tue Jan 06 00:00:00 1970 PST","Tue Jan 06 00:00:00 1970",5,"5 ",foo) | (105,5,00105,"Tue Jan 06 00:00:00 1970 PST","Tue Jan 06 00:00:00 1970",5,"5 ",foo) | 105 + (1,9) | (106,6,00106,"Wed Jan 07 00:00:00 1970 PST","Wed Jan 07 00:00:00 1970",6,"6 ",foo) | (106,6,00106,"Wed Jan 07 00:00:00 1970 PST","Wed Jan 07 00:00:00 1970",6,"6 ",foo) | 106 + (1,10) | (107,7,00107,"Thu Jan 08 00:00:00 1970 PST","Thu Jan 08 00:00:00 1970",7,"7 ",foo) | (107,7,00107,"Thu Jan 08 00:00:00 1970 PST","Thu Jan 08 00:00:00 1970",7,"7 ",foo) | 107 + (1,11) | (108,8,00108,"Fri Jan 09 00:00:00 1970 PST","Fri Jan 09 00:00:00 1970",8,"8 ",foo) | (108,8,00108,"Fri Jan 09 00:00:00 1970 PST","Fri Jan 09 00:00:00 1970",8,"8 ",foo) | 108 + (1,12) | (109,9,00109,"Sat Jan 10 00:00:00 1970 PST","Sat Jan 10 00:00:00 1970",9,"9 ",foo) | (109,9,00109,"Sat Jan 10 00:00:00 1970 PST","Sat Jan 10 00:00:00 1970",9,"9 ",foo) | 109 + (1,13) | (110,0,00110,"Sun Jan 11 00:00:00 1970 PST","Sun Jan 11 00:00:00 1970",0,"0 ",foo) | (110,0,00110,"Sun Jan 11 00:00:00 1970 PST","Sun Jan 11 00:00:00 1970",0,"0 ",foo) | 110 +(10 rows) + +-- partially unsafe to push down, not pushed down +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON t2.c1 = t2.c1 JOIN ft4 t3 ON t2.c1 = t3.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1 + -> Sort + Output: t1.c1 + Sort Key: t1.c1 + -> Nested Loop + Output: t1.c1 + -> Foreign Scan on public.ft1 t1 + Output: t1.c1 + Remote SQL: SELECT "C 1" FROM "S 1"."T 1" + -> Materialize + -> Foreign Scan + Relations: (public.ft2 t2) INNER JOIN (public.ft4 t3) + Remote SQL: SELECT NULL FROM (SELECT l.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1" WHERE (("C 1" = "C 1"))) l) l (a1) INNER JOIN (SELECT r.a9 FROM (SELECT c1 a9 FROM "S 1"."T 3") r) r (a1) ON ((l.a1 = r.a1)) +(14 rows) + +SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON t2.c1 = t2.c1 JOIN ft4 t3 ON t2.c1 = t3.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10; + c1 +---- + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 +(10 rows) + +-- SEMI JOIN, not pushed down +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +--------------------------------------------------------------------------- + Limit + Output: t1.c1 + -> Sort + Output: t1.c1 + Sort Key: t1.c1 + -> Hash Join + Output: t1.c1 + Hash Cond: (t1.c1 = t2.c1) + -> Foreign Scan on public.ft1 t1 + Output: t1.c1 + Remote SQL: SELECT "C 1" FROM "S 1"."T 1" + -> Hash + Output: t2.c1 + -> HashAggregate + Output: t2.c1 + Group Key: t2.c1 + -> Foreign Scan on public.ft2 t2 + Output: t2.c1 + Remote SQL: SELECT "C 1" FROM "S 1"."T 1" +(19 rows) + +SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10; + c1 +----- + 101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109 + 110 +(10 rows) + +-- ANTI JOIN, not pushed down +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +------------------------------------------------------------------ + Limit + Output: t1.c1 + -> Sort + Output: t1.c1 + Sort Key: t1.c1 + -> Hash Anti Join + Output: t1.c1 + Hash Cond: (t1.c1 = t2.c2) + -> Foreign Scan on public.ft1 t1 + Output: t1.c1 + Remote SQL: SELECT "C 1" FROM "S 1"."T 1" + -> Hash + Output: t2.c2 + -> Foreign Scan on public.ft2 t2 + Output: t2.c2 + Remote SQL: SELECT c2 FROM "S 1"."T 1" +(16 rows) + +SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10; + c1 +----- + 110 + 111 + 112 + 113 + 114 + 115 + 116 + 117 + 118 + 119 +(10 rows) + +-- CROSS JOIN, not pushed down +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +--------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1 + -> Sort + Output: t1.c1, t2.c1 + Sort Key: t1.c1, t2.c1 + -> Nested Loop + Output: t1.c1, t2.c1 + -> Foreign Scan on public.ft1 t1 + Output: t1.c1 + Remote SQL: SELECT "C 1" FROM "S 1"."T 1" + -> Materialize + Output: t2.c1 + -> Foreign Scan on public.ft2 t2 + Output: t2.c1 + Remote SQL: SELECT "C 1" FROM "S 1"."T 1" +(15 rows) + +SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; + c1 | c1 +----+----- + 1 | 101 + 1 | 102 + 1 | 103 + 1 | 104 + 1 | 105 + 1 | 106 + 1 | 107 + 1 | 108 + 1 | 109 + 1 | 110 +(10 rows) + +-- different server +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +------------------------------------------------------------ + Limit + Output: t1.c1, t2.c1 + -> Merge Join + Output: t1.c1, t2.c1 + Merge Cond: (t1.c1 = t2.c1) + -> Sort + Output: t1.c1 + Sort Key: t1.c1 + -> Foreign Scan on public.ft5 t1 + Output: t1.c1 + Remote SQL: SELECT c1 FROM "S 1"."T 4" + -> Sort + Output: t2.c1 + Sort Key: t2.c1 + -> Foreign Scan on public.ft6 t2 + Output: t2.c1 + Remote SQL: SELECT c1 FROM "S 1"."T 4" +(17 rows) + +SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; + c1 | c1 +----+---- +(0 rows) + +-- different effective user for permission check +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN v_ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +------------------------------------------------------------ + Limit + Output: t1.c1, ft5.c1 + -> Merge Join + Output: t1.c1, ft5.c1 + Merge Cond: (t1.c1 = ft5.c1) + -> Sort + Output: t1.c1 + Sort Key: t1.c1 + -> Foreign Scan on public.ft5 t1 + Output: t1.c1 + Remote SQL: SELECT c1 FROM "S 1"."T 4" + -> Sort + Output: ft5.c1 + Sort Key: ft5.c1 + -> Foreign Scan on public.ft5 + Output: ft5.c1 + Remote SQL: SELECT c1 FROM "S 1"."T 4" +(17 rows) + +SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN v_ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; + c1 | c1 +----+---- +(0 rows) + +-- unsafe join conditions +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c8 = t2.c8) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +----------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1, t1.c3 + -> Sort + Output: t1.c1, t2.c1, t1.c3 + Sort Key: t1.c3, t1.c1 + -> Merge Join + Output: t1.c1, t2.c1, t1.c3 + Merge Cond: (t1.c8 = t2.c8) + -> Sort + Output: t1.c1, t1.c3, t1.c8 + Sort Key: t1.c8 + -> Foreign Scan on public.ft1 t1 + Output: t1.c1, t1.c3, t1.c8 + Remote SQL: SELECT "C 1", c3, c8 FROM "S 1"."T 1" + -> Sort + Output: t2.c1, t2.c8 + Sort Key: t2.c8 + -> Foreign Scan on public.ft2 t2 + Output: t2.c1, t2.c8 + Remote SQL: SELECT "C 1", c8 FROM "S 1"."T 1" +(20 rows) + +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c8 = t2.c8) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + c1 | c1 +----+----- + 1 | 102 + 1 | 103 + 1 | 104 + 1 | 105 + 1 | 106 + 1 | 107 + 1 | 108 + 1 | 109 + 1 | 110 + 1 | 1 +(10 rows) + +-- local filter (unsafe conditions on one side) +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = 'foo' ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + QUERY PLAN +----------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1, t1.c3 + -> Sort + Output: t1.c1, t2.c1, t1.c3 + Sort Key: t1.c3, t1.c1 + -> Hash Join + Output: t1.c1, t2.c1, t1.c3 + Hash Cond: (t2.c1 = t1.c1) + -> Foreign Scan on public.ft2 t2 + Output: t2.c1 + Remote SQL: SELECT "C 1" FROM "S 1"."T 1" + -> Hash + Output: t1.c1, t1.c3 + -> Foreign Scan on public.ft1 t1 + Output: t1.c1, t1.c3 + Filter: (t1.c8 = 'foo'::user_enum) + Remote SQL: SELECT "C 1", c3, c8 FROM "S 1"."T 1" +(17 rows) + +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = 'foo' ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; + c1 | c1 +-----+----- + 101 | 101 + 102 | 102 + 103 | 103 + 104 | 104 + 105 | 105 + 106 | 106 + 107 | 107 + 108 | 108 + 109 | 109 + 110 | 110 +(10 rows) + +-- Aggregate after UNION, for testing setrefs +EXPLAIN (COSTS false, VERBOSE) +SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, (avg((t1.c1 + t2.c1))) + -> Sort + Output: t1.c1, (avg((t1.c1 + t2.c1))) + Sort Key: t1.c1 + -> HashAggregate + Output: t1.c1, avg((t1.c1 + t2.c1)) + Group Key: t1.c1 + -> HashAggregate + Output: t1.c1, t2.c1 + Group Key: t1.c1, t2.c1 + -> Append + -> Foreign Scan + Output: t1.c1, t2.c1 + Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) + Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a10 FROM (SELECT "C 1" a10 FROM "S 1"."T 1") l) l (a1) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1)) + -> Foreign Scan + Output: t1_1.c1, t2_1.c1 + Relations: (public.ft1 t1) INNER JOIN (public.ft2 t2) + Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a10 FROM (SELECT "C 1" a10 FROM "S 1"."T 1") l) l (a1) INNER JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1)) +(20 rows) + +SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10; + t1c1 | avg +------+---------------------- + 101 | 202.0000000000000000 + 102 | 204.0000000000000000 + 103 | 206.0000000000000000 + 104 | 208.0000000000000000 + 105 | 210.0000000000000000 + 106 | 212.0000000000000000 + 107 | 214.0000000000000000 + 108 | 216.0000000000000000 + 109 | 218.0000000000000000 + 110 | 220.0000000000000000 +(10 rows) + +-- join two foreign tables and two local tables +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON t1.c1 = t2.c1 JOIN "S 1"."T 1" t3 ON t1.c1 = t3."C 1" JOIN "S 1"."T 2" t4 ON t1.c1 = t4.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10; + QUERY PLAN +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: t1.c1, t2.c1 + -> Sort + Output: t1.c1, t2.c1 + Sort Key: t1.c1 + -> Hash Join + Output: t1.c1, t2.c1 + Hash Cond: (t1.c1 = t3."C 1") + -> Foreign Scan + Output: t1.c1, t2.c1 + Relations: (public.ft1 t1) LEFT JOIN (public.ft2 t2) + Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a10 FROM (SELECT "C 1" a10 FROM "S 1"."T 1") l) l (a1) LEFT JOIN (SELECT r.a9 FROM (SELECT "C 1" a9 FROM "S 1"."T 1") r) r (a1) ON ((l.a1 = r.a1)) + -> Hash + Output: t3."C 1", t4.c1 + -> Merge Join + Output: t3."C 1", t4.c1 + Merge Cond: (t3."C 1" = t4.c1) + -> Index Only Scan using t1_pkey on "S 1"."T 1" t3 + Output: t3."C 1" + -> Sort + Output: t4.c1 + Sort Key: t4.c1 + -> Seq Scan on "S 1"."T 2" t4 + Output: t4.c1 +(24 rows) + +SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON t1.c1 = t2.c1 JOIN "S 1"."T 1" t3 ON t1.c1 = t3."C 1" JOIN "S 1"."T 2" t4 ON t1.c1 = t4.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10; + c1 | c1 +----+---- + 11 | 11 + 12 | 12 + 13 | 13 + 14 | 14 + 15 | 15 + 16 | 16 + 17 | 17 + 18 | 18 + 19 | 19 + 20 | 20 +(10 rows) + +-- =================================================================== -- parameterized queries -- =================================================================== -- simple join @@ -1210,22 +1908,15 @@ UPDATE ft2 SET c2 = c2 + 400, c3 = c3 || '_update7' WHERE c1 % 10 = 7 RETURNING EXPLAIN (verbose, costs off) UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9; - QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Update on public.ft2 Remote SQL: UPDATE "S 1"."T 1" SET c2 = $2, c3 = $3, c7 = $4 WHERE ctid = $1 - -> Hash Join + -> Foreign Scan Output: ft2.c1, (ft2.c2 + 500), NULL::integer, (ft2.c3 || '_update9'::text), ft2.c4, ft2.c5, ft2.c6, 'ft2 '::character(10), ft2.c8, ft2.ctid, ft1.* - Hash Cond: (ft2.c2 = ft1.c1) - -> Foreign Scan on public.ft2 - Output: ft2.c1, ft2.c2, ft2.c3, ft2.c4, ft2.c5, ft2.c6, ft2.c8, ft2.ctid - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c8, ctid FROM "S 1"."T 1" FOR UPDATE - -> Hash - Output: ft1.*, ft1.c1 - -> Foreign Scan on public.ft1 - Output: ft1.*, ft1.c1 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9)) -(13 rows) + Relations: (public.ft2) INNER JOIN (public.ft1) + Remote SQL: SELECT l.a1, l.a2, l.a3, l.a4, l.a5, l.a6, l.a7, l.a8, r.a1 FROM (SELECT l.a9, l.a10, l.a12, l.a13, l.a14, l.a15, l.a17, l.a7 FROM (SELECT "C 1" a9, c2 a10, c3 a12, c4 a13, c5 a14, c6 a15, c8 a17, ctid a7 FROM "S 1"."T 1" FOR UPDATE) l) l (a1, a2, a3, a4, a5, a6, a7, a8) INNER JOIN (SELECT ROW(r.a10, r.a11, r.a12, r.a13, r.a14, r.a15, r.a16, r.a17), r.a10 FROM (SELECT "C 1" a10, c2 a11, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 9))) r) r (a1, a2) ON ((l.a2 = r.a2)) +(6 rows) UPDATE ft2 SET c2 = ft2.c2 + 500, c3 = ft2.c3 || '_update9', c7 = DEFAULT FROM ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 9; @@ -1351,22 +2042,15 @@ DELETE FROM ft2 WHERE c1 % 10 = 5 RETURNING c1, c4; EXPLAIN (verbose, costs off) DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2; - QUERY PLAN ----------------------------------------------------------------------------------------------------------------------- + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Delete on public.ft2 Remote SQL: DELETE FROM "S 1"."T 1" WHERE ctid = $1 - -> Hash Join + -> Foreign Scan Output: ft2.ctid, ft1.* - Hash Cond: (ft2.c2 = ft1.c1) - -> Foreign Scan on public.ft2 - Output: ft2.ctid, ft2.c2 - Remote SQL: SELECT c2, ctid FROM "S 1"."T 1" FOR UPDATE - -> Hash - Output: ft1.*, ft1.c1 - -> Foreign Scan on public.ft1 - Output: ft1.*, ft1.c1 - Remote SQL: SELECT "C 1", c2, c3, c4, c5, c6, c7, c8 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2)) -(13 rows) + Relations: (public.ft2) INNER JOIN (public.ft1) + Remote SQL: SELECT l.a1, r.a1 FROM (SELECT l.a7, l.a10 FROM (SELECT c2 a10, ctid a7 FROM "S 1"."T 1" FOR UPDATE) l) l (a1, a2) INNER JOIN (SELECT ROW(r.a10, r.a11, r.a12, r.a13, r.a14, r.a15, r.a16, r.a17), r.a10 FROM (SELECT "C 1" a10, c2 a11, c3 a12, c4 a13, c5 a14, c6 a15, c7 a16, c8 a17 FROM "S 1"."T 1" WHERE ((("C 1" % 10) = 2))) r) r (a1, a2) ON ((l.a2 = r.a2)) +(6 rows) DELETE FROM ft2 USING ft1 WHERE ft1.c1 = ft2.c2 AND ft1.c1 % 10 = 2; SELECT c1,c2,c3,c4 FROM ft2 ORDER BY c1; @@ -3641,3 +4325,6 @@ QUERY: CREATE FOREIGN TABLE t5 ( OPTIONS (schema_name 'import_source', table_name 't5'); CONTEXT: importing foreign table "t5" ROLLBACK; +-- Cleanup +DROP OWNED BY view_owner; +DROP USER view_owner; diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 6da01e1..05565e6 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -28,7 +28,6 @@ #include "optimizer/pathnode.h" #include "optimizer/paths.h" #include "optimizer/planmain.h" -#include "optimizer/prep.h" #include "optimizer/restrictinfo.h" #include "optimizer/var.h" #include "parser/parsetree.h" @@ -48,41 +47,8 @@ PG_MODULE_MAGIC; #define DEFAULT_FDW_TUPLE_COST 0.01 /* - * FDW-specific planner information kept in RelOptInfo.fdw_private for a - * foreign table. This information is collected by postgresGetForeignRelSize. - */ -typedef struct PgFdwRelationInfo -{ - /* baserestrictinfo clauses, broken down into safe and unsafe subsets. */ - List *remote_conds; - List *local_conds; - - /* Bitmap of attr numbers we need to fetch from the remote server. */ - Bitmapset *attrs_used; - - /* Cost and selectivity of local_conds. */ - QualCost local_conds_cost; - Selectivity local_conds_sel; - - /* Estimated size and cost for a scan with baserestrictinfo quals. */ - double rows; - int width; - Cost startup_cost; - Cost total_cost; - - /* Options extracted from catalogs. */ - bool use_remote_estimate; - Cost fdw_startup_cost; - Cost fdw_tuple_cost; - - /* Cached catalog information. */ - ForeignTable *table; - ForeignServer *server; - UserMapping *user; /* only set in use_remote_estimate mode */ -} PgFdwRelationInfo; - -/* - * Indexes of FDW-private information stored in fdw_private lists. + * Indexes of FDW-private information stored in fdw_private of ForeignScan of + * a simple foreign table scan for a SELECT statement. * * We store various information in ForeignScan.fdw_private to pass it from * planner to executor. Currently we store: @@ -99,7 +65,13 @@ enum FdwScanPrivateIndex /* SQL statement to execute remotely (as a String node) */ FdwScanPrivateSelectSql, /* Integer list of attribute numbers retrieved by the SELECT */ - FdwScanPrivateRetrievedAttrs + FdwScanPrivateRetrievedAttrs, + /* Integer value of server for the scan */ + FdwScanPrivateServerOid, + /* Integer value of user mapping for the scan */ + FdwScanPrivateUserMappingOid, + /* Names of relation scanned, added when the scan is join */ + FdwScanPrivateRelations, }; /* @@ -129,7 +101,8 @@ enum FdwModifyPrivateIndex */ typedef struct PgFdwScanState { - Relation rel; /* relcache entry for the foreign table */ + const char *relname; /* name of relation being scanned */ + TupleDesc tupdesc; /* tuple descriptor of the scan */ AttInMetadata *attinmeta; /* attribute datatype conversion metadata */ /* extracted fdw_private data */ @@ -195,6 +168,8 @@ typedef struct PgFdwAnalyzeState AttInMetadata *attinmeta; /* attribute datatype conversion metadata */ List *retrieved_attrs; /* attr numbers retrieved by query */ + char *query; /* text of SELECT command */ + /* collected sample rows */ HeapTuple *rows; /* array of size targrows */ int targrows; /* target # of sample rows */ @@ -215,7 +190,10 @@ typedef struct PgFdwAnalyzeState */ typedef struct ConversionLocation { - Relation rel; /* foreign table's relcache entry */ + const char *relname; /* name of relation being processed, or NULL for + a foreign join */ + const char *query; /* query being processed */ + TupleDesc tupdesc; /* tuple descriptor for attribute names */ AttrNumber cur_attno; /* attribute number being processed, or 0 */ } ConversionLocation; @@ -289,6 +267,12 @@ static bool postgresAnalyzeForeignTable(Relation relation, BlockNumber *totalpages); static List *postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid); +static void postgresGetForeignJoinPaths(PlannerInfo *root, + RelOptInfo *joinrel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + JoinType jointype, + JoinPathExtraData *extra); /* * Helper functions @@ -324,12 +308,40 @@ static void analyze_row_processor(PGresult *res, int row, PgFdwAnalyzeState *astate); static HeapTuple make_tuple_from_result_row(PGresult *res, int row, - Relation rel, + const char *relname, + const char *query, + TupleDesc tupdesc, AttInMetadata *attinmeta, List *retrieved_attrs, MemoryContext temp_context); static void conversion_error_callback(void *arg); +/* + * Describe Bitmapset as comma-separated integer list. + * For debug purpose. + * XXX Can this become a member of bitmapset.c? + */ +static char * +bms_to_str(Bitmapset *bmp) +{ + StringInfoData buf; + bool first = true; + int x; + + initStringInfo(&buf); + + x = -1; + while ((x = bms_next_member(bmp, x)) >= 0) + { + if (!first) + appendStringInfoString(&buf, ", "); + appendStringInfo(&buf, "%d", x); + + first = false; + } + + return buf.data; +} /* * Foreign-data wrapper handler function: return a struct with pointers @@ -369,6 +381,9 @@ postgres_fdw_handler(PG_FUNCTION_ARGS) /* Support functions for IMPORT FOREIGN SCHEMA */ routine->ImportForeignSchema = postgresImportForeignSchema; + /* Support functions for join push-down */ + routine->GetForeignJoinPaths = postgresGetForeignJoinPaths; + PG_RETURN_POINTER(routine); } @@ -394,9 +409,13 @@ postgresGetForeignRelSize(PlannerInfo *root, fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo)); baserel->fdw_private = (void *) fpinfo; + /* This scan can be pushed down to the remote. */ + fpinfo->pushdown_safe = true; + /* Look up foreign-table catalog info. */ fpinfo->table = GetForeignTable(foreigntableid); fpinfo->server = GetForeignServer(fpinfo->table->serverid); + fpinfo->umid = baserel->umid; /* * Extract user-settable option values. Note that per-table setting of @@ -429,22 +448,6 @@ postgresGetForeignRelSize(PlannerInfo *root, } /* - * If the table or the server is configured to use remote estimates, - * identify which user to do remote access as during planning. This - * should match what ExecCheckRTEPerms() does. If we fail due to lack of - * permissions, the query would have failed at runtime anyway. - */ - if (fpinfo->use_remote_estimate) - { - RangeTblEntry *rte = planner_rt_fetch(baserel->relid, root); - Oid userid = rte->checkAsUser ? rte->checkAsUser : GetUserId(); - - fpinfo->user = GetUserMapping(userid, fpinfo->server->serverid); - } - else - fpinfo->user = NULL; - - /* * Identify which baserestrictinfo clauses can be sent to the remote * server and which can't. */ @@ -753,6 +756,9 @@ postgresGetForeignPlan(PlannerInfo *root, List *retrieved_attrs; StringInfoData sql; ListCell *lc; + List *fdw_scan_tlist = NIL; + ForeignScan *scan; + StringInfoData relations; /* * Separate the scan_clauses into those that can be executed remotely and @@ -770,8 +776,8 @@ postgresGetForeignPlan(PlannerInfo *root, * This code must match "extract_actual_clauses(scan_clauses, false)" * except for the additional decision about remote versus local execution. * Note however that we only strip the RestrictInfo nodes from the - * local_exprs list, since appendWhereClause expects a list of - * RestrictInfos. + * local_exprs list, though appendConditions expects a list of + * RestrictInfos or Exprs. */ foreach(lc, scan_clauses) { @@ -795,71 +801,27 @@ postgresGetForeignPlan(PlannerInfo *root, /* * Build the query string to be sent for execution, and identify - * expressions to be sent as parameters. + * expressions to be sent as parameters. If the relation to scan is a join + * relation, receive constructed relations string from deparseSelectSql. */ initStringInfo(&sql); - deparseSelectSql(&sql, root, baserel, fpinfo->attrs_used, - &retrieved_attrs); - if (remote_conds) - appendWhereClause(&sql, root, baserel, remote_conds, - true, ¶ms_list); - - /* - * Add FOR UPDATE/SHARE if appropriate. We apply locking during the - * initial row fetch, rather than later on as is done for local tables. - * The extra roundtrips involved in trying to duplicate the local - * semantics exactly don't seem worthwhile (see also comments for - * RowMarkType). - * - * Note: because we actually run the query as a cursor, this assumes that - * DECLARE CURSOR ... FOR UPDATE is supported, which it isn't before 8.3. - */ - if (baserel->relid == root->parse->resultRelation && - (root->parse->commandType == CMD_UPDATE || - root->parse->commandType == CMD_DELETE)) - { - /* Relation is UPDATE/DELETE target, so use FOR UPDATE */ - appendStringInfoString(&sql, " FOR UPDATE"); - } - else - { - PlanRowMark *rc = get_plan_rowmark(root->rowMarks, baserel->relid); - - if (rc) - { - /* - * Relation is specified as a FOR UPDATE/SHARE target, so handle - * that. (But we could also see LCS_NONE, meaning this isn't a - * target relation after all.) - * - * For now, just ignore any [NO] KEY specification, since (a) it's - * not clear what that means for a remote table that we don't have - * complete information about, and (b) it wouldn't work anyway on - * older remote servers. Likewise, we don't worry about NOWAIT. - */ - switch (rc->strength) - { - case LCS_NONE: - /* No locking needed */ - break; - case LCS_FORKEYSHARE: - case LCS_FORSHARE: - appendStringInfoString(&sql, " FOR SHARE"); - break; - case LCS_FORNOKEYUPDATE: - case LCS_FORUPDATE: - appendStringInfoString(&sql, " FOR UPDATE"); - break; - } - } - } + if (baserel->reloptkind == RELOPT_JOINREL) + initStringInfo(&relations); + deparseSelectSql(&sql, root, baserel, fpinfo->attrs_used, remote_conds, + ¶ms_list, &fdw_scan_tlist, &retrieved_attrs, + baserel->reloptkind == RELOPT_JOINREL ? &relations : NULL, + false); /* - * Build the fdw_private list that will be available to the executor. + * Build the fdw_private list that will be available in the executor. * Items in the list must match enum FdwScanPrivateIndex, above. */ - fdw_private = list_make2(makeString(sql.data), - retrieved_attrs); + fdw_private = list_make4(makeString(sql.data), + retrieved_attrs, + makeInteger(fpinfo->server->serverid), + makeInteger(fpinfo->umid)); + if (baserel->reloptkind == RELOPT_JOINREL) + fdw_private = lappend(fdw_private, makeString(relations.data)); /* * Create the ForeignScan node from target list, local filtering @@ -869,12 +831,14 @@ postgresGetForeignPlan(PlannerInfo *root, * field of the finished plan node; we can't keep them in private state * because then they wouldn't be subject to later planner processing. */ - return make_foreignscan(tlist, + scan = make_foreignscan(tlist, local_exprs, scan_relid, params_list, fdw_private, - NIL /* no custom tlist */ ); + fdw_scan_tlist); + + return scan; } /* @@ -887,9 +851,8 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) ForeignScan *fsplan = (ForeignScan *) node->ss.ps.plan; EState *estate = node->ss.ps.state; PgFdwScanState *fsstate; - RangeTblEntry *rte; - Oid userid; - ForeignTable *table; + Oid serverid; + Oid umid; ForeignServer *server; UserMapping *user; int numParams; @@ -909,22 +872,13 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) node->fdw_state = (void *) fsstate; /* - * Identify which user to do the remote access as. This should match what - * ExecCheckRTEPerms() does. - */ - rte = rt_fetch(fsplan->scan.scanrelid, estate->es_range_table); - userid = rte->checkAsUser ? rte->checkAsUser : GetUserId(); - - /* Get info about foreign table. */ - fsstate->rel = node->ss.ss_currentRelation; - table = GetForeignTable(RelationGetRelid(fsstate->rel)); - server = GetForeignServer(table->serverid); - user = GetUserMapping(userid, server->serverid); - - /* * Get connection to the foreign server. Connection manager will * establish new connection if necessary. */ + serverid = intVal(list_nth(fsplan->fdw_private, FdwScanPrivateServerOid)); + umid = intVal(list_nth(fsplan->fdw_private, FdwScanPrivateUserMappingOid)); + server = GetForeignServer(serverid); + user = GetUserMappingById(umid); fsstate->conn = GetConnection(server, user, false); /* Assign a unique ID for my cursor */ @@ -949,8 +903,18 @@ postgresBeginForeignScan(ForeignScanState *node, int eflags) ALLOCSET_SMALL_INITSIZE, ALLOCSET_SMALL_MAXSIZE); - /* Get info we'll need for input data conversion. */ - fsstate->attinmeta = TupleDescGetAttInMetadata(RelationGetDescr(fsstate->rel)); + /* Get info we'll need for input data conversion and error report. */ + if (fsplan->scan.scanrelid > 0) + { + fsstate->relname = RelationGetRelationName(node->ss.ss_currentRelation); + fsstate->tupdesc = RelationGetDescr(node->ss.ss_currentRelation); + } + else + { + fsstate->relname = NULL; + fsstate->tupdesc = node->ss.ss_ScanTupleSlot->tts_tupleDescriptor; + } + fsstate->attinmeta = TupleDescGetAttInMetadata(fsstate->tupdesc); /* Prepare for output conversion of parameters used in remote query. */ numParams = list_length(fsplan->fdw_exprs); @@ -1679,10 +1643,25 @@ postgresExplainForeignScan(ForeignScanState *node, ExplainState *es) { List *fdw_private; char *sql; + char *relations; + + fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; + + /* + * Add names of relation handled by the foreign scan when the scan is a + * join + */ + if (list_length(fdw_private) > FdwScanPrivateRelations) + { + relations = strVal(list_nth(fdw_private, FdwScanPrivateRelations)); + ExplainPropertyText("Relations", relations, es); + } + /* + * Add remote query, when VERBOSE option is specified. + */ if (es->verbose) { - fdw_private = ((ForeignScan *) node->ss.ps.plan)->fdw_private; sql = strVal(list_nth(fdw_private, FdwScanPrivateSelectSql)); ExplainPropertyText("Remote SQL", sql, es); } @@ -1741,10 +1720,12 @@ estimate_path_cost_size(PlannerInfo *root, */ if (fpinfo->use_remote_estimate) { + List *remote_conds; List *remote_join_conds; List *local_join_conds; StringInfoData sql; List *retrieved_attrs; + UserMapping *user; PGconn *conn; Selectivity local_sel; QualCost local_cost; @@ -1756,24 +1737,24 @@ estimate_path_cost_size(PlannerInfo *root, classifyConditions(root, baserel, join_conds, &remote_join_conds, &local_join_conds); + remote_conds = copyObject(fpinfo->remote_conds); + remote_conds = list_concat(remote_conds, remote_join_conds); + /* * Construct EXPLAIN query including the desired SELECT, FROM, and * WHERE clauses. Params and other-relation Vars are replaced by * dummy values. + * Here we waste params_list and fdw_scan_tlist because they are + * unnecessary for EXPLAIN. */ initStringInfo(&sql); appendStringInfoString(&sql, "EXPLAIN "); - deparseSelectSql(&sql, root, baserel, fpinfo->attrs_used, - &retrieved_attrs); - if (fpinfo->remote_conds) - appendWhereClause(&sql, root, baserel, fpinfo->remote_conds, - true, NULL); - if (remote_join_conds) - appendWhereClause(&sql, root, baserel, remote_join_conds, - (fpinfo->remote_conds == NIL), NULL); + deparseSelectSql(&sql, root, baserel, fpinfo->attrs_used, remote_conds, + NULL, NULL, &retrieved_attrs, NULL, false); /* Get the remote estimate */ - conn = GetConnection(fpinfo->server, fpinfo->user, false); + user = GetUserMappingById(fpinfo->umid); + conn = GetConnection(fpinfo->server, user, false); get_remote_estimate(sql.data, conn, &rows, &width, &startup_cost, &total_cost); ReleaseConnection(conn); @@ -2070,7 +2051,9 @@ fetch_more_data(ForeignScanState *node) { fsstate->tuples[i] = make_tuple_from_result_row(res, i, - fsstate->rel, + fsstate->relname, + fsstate->query, + fsstate->tupdesc, fsstate->attinmeta, fsstate->retrieved_attrs, fsstate->temp_cxt); @@ -2288,7 +2271,9 @@ store_returning_result(PgFdwModifyState *fmstate, HeapTuple newtup; newtup = make_tuple_from_result_row(res, 0, - fmstate->rel, + RelationGetRelationName(fmstate->rel), + fmstate->query, + RelationGetDescr(fmstate->rel), fmstate->attinmeta, fmstate->retrieved_attrs, fmstate->temp_cxt); @@ -2438,6 +2423,7 @@ postgresAcquireSampleRowsFunc(Relation relation, int elevel, initStringInfo(&sql); appendStringInfo(&sql, "DECLARE c%u CURSOR FOR ", cursor_number); deparseAnalyzeSql(&sql, relation, &astate.retrieved_attrs); + astate.query = sql.data; /* In what follows, do not risk leaking any PGresults. */ PG_TRY(); @@ -2579,7 +2565,9 @@ analyze_row_processor(PGresult *res, int row, PgFdwAnalyzeState *astate) oldcontext = MemoryContextSwitchTo(astate->anl_cxt); astate->rows[pos] = make_tuple_from_result_row(res, row, - astate->rel, + RelationGetRelationName(astate->rel), + astate->query, + RelationGetDescr(astate->rel), astate->attinmeta, astate->retrieved_attrs, astate->temp_cxt); @@ -2853,6 +2841,280 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) } /* + * Construct PgFdwRelationInfo from two join sources + */ +static void +merge_fpinfo(RelOptInfo *outerrel, + RelOptInfo *innerrel, + PgFdwRelationInfo *fpinfo, + JoinType jointype, + double rows, + int width) +{ + PgFdwRelationInfo *fpinfo_o; + PgFdwRelationInfo *fpinfo_i; + + fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private; + fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private; + + /* Mark that this join can be pushed down safely */ + fpinfo->pushdown_safe = true; + + /* Join relation must have conditions come from sources */ + fpinfo->remote_conds = list_concat(copyObject(fpinfo_o->remote_conds), + copyObject(fpinfo_i->remote_conds)); + fpinfo->local_conds = list_concat(copyObject(fpinfo_o->local_conds), + copyObject(fpinfo_i->local_conds)); + + /* Only for simple foreign table scan */ + fpinfo->attrs_used = NULL; + + /* rows and width will be set later */ + fpinfo->rows = rows; + fpinfo->width = width; + + /* A join have local conditions for outer and inner, so sum up them. */ + fpinfo->local_conds_cost.startup = fpinfo_o->local_conds_cost.startup + + fpinfo_i->local_conds_cost.startup; + fpinfo->local_conds_cost.per_tuple = fpinfo_o->local_conds_cost.per_tuple + + fpinfo_i->local_conds_cost.per_tuple; + + /* Don't consider correlation between local filters. */ + fpinfo->local_conds_sel = fpinfo_o->local_conds_sel * + fpinfo_i->local_conds_sel; + + fpinfo->use_remote_estimate = false; + + /* + * These two comes default or per-server setting, so outer and inner must + * have same value. + */ + fpinfo->fdw_startup_cost = fpinfo_o->fdw_startup_cost; + fpinfo->fdw_tuple_cost = fpinfo_o->fdw_tuple_cost; + + /* + * TODO estimate more accurately + */ + fpinfo->startup_cost = fpinfo->fdw_startup_cost + + fpinfo->local_conds_cost.startup; + fpinfo->total_cost = fpinfo->startup_cost + + (fpinfo->fdw_tuple_cost + + fpinfo->local_conds_cost.per_tuple + + cpu_tuple_cost) * fpinfo->rows; + + /* serverid and userid are respectively identical */ + fpinfo->server = fpinfo_o->server; + fpinfo->umid = fpinfo_o->umid; + + fpinfo->outerrel = outerrel; + fpinfo->innerrel = innerrel; + fpinfo->jointype = jointype; + + /* This join can be pushed down safely */ + fpinfo->pushdown_safe = true; + + /* joinclauses and otherclauses will be set later */ +} + +/* + * postgresGetForeignJoinPaths + * Add possible ForeignPath to joinrel. + * + * Joins satisfy conditions below can be pushed down to the remote PostgreSQL + * server. + * + * 1) Join type is INNER or OUTER (one of LEFT/RIGHT/FULL) + * 2) Both outer and inner portions are safe to push-down + * 3) All foreign tables in the join belong to the same foreign server + * 4) All join conditions are safe to push down + * 5) No relation has local filter (this can be relaxed for INNER JOIN with + * no volatile function/operator, but as of now we want safer way) + */ +static void +postgresGetForeignJoinPaths(PlannerInfo *root, + RelOptInfo *joinrel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + JoinType jointype, + JoinPathExtraData *extra) +{ + PgFdwRelationInfo *fpinfo; + PgFdwRelationInfo *fpinfo_o; + PgFdwRelationInfo *fpinfo_i; + ForeignPath *joinpath; + double rows; + Cost startup_cost; + Cost total_cost; + + ListCell *lc; + List *joinclauses; + List *otherclauses; + + /* + * Skip if this join combination has been considered already. + */ + if (joinrel->fdw_private) + { + ereport(DEBUG3, (errmsg("combination already considered"))); + return; + } + + /* + * Create unfinished PgFdwRelationInfo entry which is used to indicate that + * the join relaiton is already considered but the join can't be pushed + * down. Once we know that this join can be pushed down, we fill the entry + * and make it valid by calling merge_fpinfo. + * + * This unfinished entry prevents redandunt checks for a join combination + * which is already known as unsafe to push down. + */ + fpinfo = (PgFdwRelationInfo *) palloc0(sizeof(PgFdwRelationInfo)); + fpinfo->pushdown_safe = false; + joinrel->fdw_private = fpinfo; + + /* + * We support all outer joins in addition to inner join. CROSS JOIN is + * an INNER JOIN with no conditions internally, so will be checked later. + */ + if (jointype != JOIN_INNER && jointype != JOIN_LEFT && + jointype != JOIN_RIGHT && jointype != JOIN_FULL) + { + ereport(DEBUG3, (errmsg("unsupported join type (SEMI, ANTI)"))); + return; + } + + /* + * Having valid PgFdwRelationInfo marked as "safe to push down" in + * RelOptInfo#fdw_private indicates that scanning against the relation can + * be pushed down. If either of them doesn't have PgFdwRelationInfo or it + * is not marked as safe, give up to push down this join relation. + */ + fpinfo_o = (PgFdwRelationInfo *) outerrel->fdw_private; + if (!fpinfo_o || !fpinfo_o->pushdown_safe) + { + ereport(DEBUG3, (errmsg("outer is not safe to push-down"))); + return; + } + fpinfo_i = (PgFdwRelationInfo *) innerrel->fdw_private; + if (!fpinfo_i || !fpinfo_i->pushdown_safe) + { + ereport(DEBUG3, (errmsg("inner is not safe to push-down"))); + return; + } + + /* + * All relations in the join must belong to same server. Having a valid + * fdw_private means that all relations in the relations belong to the + * server the fdw_private has, so what we should do is just compare + * serverid of outer/inner relations. + */ + if (fpinfo_o->server->serverid != fpinfo_i->server->serverid) + { + ereport(DEBUG3, (errmsg("server unmatch"))); + return; + } + + /* + * No source relation can have local conditions. This can be relaxed + * if the join is an inner join and local conditions don't contain + * volatile function/operator, but as of now we leave it as future + * enhancement. + */ + if (fpinfo_o->local_conds != NULL || fpinfo_i->local_conds != NULL) + { + ereport(DEBUG3, (errmsg("join with local filter"))); + return; + } + + /* + * Separate restrictlist into two lists, join conditions and remote filters. + */ + joinclauses = extra->restrictlist; + if (IS_OUTER_JOIN(jointype)) + { + extract_actual_join_clauses(joinclauses, &joinclauses, &otherclauses); + } + else + { + joinclauses = extract_actual_clauses(joinclauses, false); + otherclauses = NIL; + } + + /* + * Note that CROSS JOIN (cartesian product) is transformed to JOIN_INNER + * with empty joinclauses. Pushing down CROSS JOIN usually produces more + * result than retrieving each tables separately, so we don't push down + * such joins. + */ + if (jointype == JOIN_INNER && joinclauses == NIL) + { + ereport(DEBUG3, (errmsg("unsupported join type (CROSS)"))); + return; + } + + /* + * Join condition must be safe to push down. + */ + foreach(lc, joinclauses) + { + Expr *expr = (Expr *) lfirst(lc); + + if (!is_foreign_expr(root, joinrel, expr)) + { + ereport(DEBUG3, (errmsg("join quals contains unsafe conditions"))); + return; + } + } + + /* + * Other condition for the join must be safe to push down. + */ + foreach(lc, otherclauses) + { + Expr *expr = (Expr *) lfirst(lc); + + if (!is_foreign_expr(root, joinrel, expr)) + { + ereport(DEBUG3, (errmsg("filter contains unsafe conditions"))); + return; + } + } + + /* Here we know that this join can be pushed-down to remote side. */ + + /* Construct fpinfo for the join relation */ + merge_fpinfo(outerrel, innerrel, fpinfo, jointype, joinrel->rows, + joinrel->width); + fpinfo->joinclauses = joinclauses; + fpinfo->otherclauses = otherclauses; + + /* TODO determine more accurate cost and rows of the join. */ + rows = joinrel->rows; + startup_cost = fpinfo->startup_cost; + total_cost = fpinfo->total_cost; + + /* + * Create a new join path and add it to the joinrel which represents a join + * between foreign tables. + */ + joinpath = create_foreignscan_path(root, + joinrel, + rows, + startup_cost, + total_cost, + NIL, /* no pathkeys */ + NULL, /* no required_outer */ + NIL); /* no fdw_private */ + + /* Add generated path into joinrel by add_path(). */ + add_path(joinrel, (Path *) joinpath); + elog(DEBUG3, "join path added for (%s) join (%s)", + bms_to_str(outerrel->relids), bms_to_str(innerrel->relids)); + + /* TODO consider parameterized paths */ +} + +/* * Create a tuple from the specified row of the PGresult. * * rel is the local representation of the foreign table, attinmeta is @@ -2863,13 +3125,14 @@ postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid) static HeapTuple make_tuple_from_result_row(PGresult *res, int row, - Relation rel, + const char *relname, + const char *query, + TupleDesc tupdesc, AttInMetadata *attinmeta, List *retrieved_attrs, MemoryContext temp_context) { HeapTuple tuple; - TupleDesc tupdesc = RelationGetDescr(rel); Datum *values; bool *nulls; ItemPointer ctid = NULL; @@ -2896,7 +3159,9 @@ make_tuple_from_result_row(PGresult *res, /* * Set up and install callback to report where conversion error occurs. */ - errpos.rel = rel; + errpos.relname = relname; + errpos.query = query; + errpos.tupdesc = tupdesc; errpos.cur_attno = 0; errcallback.callback = conversion_error_callback; errcallback.arg = (void *) &errpos; @@ -2986,11 +3251,39 @@ make_tuple_from_result_row(PGresult *res, static void conversion_error_callback(void *arg) { + const char *attname; + const char *relname; ConversionLocation *errpos = (ConversionLocation *) arg; - TupleDesc tupdesc = RelationGetDescr(errpos->rel); + TupleDesc tupdesc = errpos->tupdesc; + StringInfoData buf; + + if (errpos->relname) + { + /* error occurred in a scan against a foreign table */ + initStringInfo(&buf); + if (errpos->cur_attno > 0) + appendStringInfo(&buf, "column \"%s\"", + NameStr(tupdesc->attrs[errpos->cur_attno - 1]->attname)); + else if (errpos->cur_attno == SelfItemPointerAttributeNumber) + appendStringInfoString(&buf, "column \"ctid\""); + attname = buf.data; + + initStringInfo(&buf); + appendStringInfo(&buf, "foreign table \"%s\"", errpos->relname); + relname = buf.data; + } + else + { + /* error occurred in a scan against a foreign join */ + initStringInfo(&buf); + appendStringInfo(&buf, "column %d", errpos->cur_attno - 1); + attname = buf.data; + + initStringInfo(&buf); + appendStringInfo(&buf, "foreign join \"%s\"", errpos->query); + relname = buf.data; + } if (errpos->cur_attno > 0 && errpos->cur_attno <= tupdesc->natts) - errcontext("column \"%s\" of foreign table \"%s\"", - NameStr(tupdesc->attrs[errpos->cur_attno - 1]->attname), - RelationGetRelationName(errpos->rel)); + errcontext("%s of %s", attname, relname); } diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index 3835ddb..82ce480 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -16,10 +16,59 @@ #include "foreign/foreign.h" #include "lib/stringinfo.h" #include "nodes/relation.h" +#include "nodes/plannodes.h" #include "utils/relcache.h" #include "libpq-fe.h" +/* + * FDW-specific planner information kept in RelOptInfo.fdw_private for a + * foreign table or a foreign join. This information is collected by + * postgresGetForeignRelSize, or calculated from join source relations. + */ +typedef struct PgFdwRelationInfo +{ + /* + * True means that the relation can be pushed down. Always true for + * simple foreign scan. + */ + bool pushdown_safe; + + /* baserestrictinfo clauses, broken down into safe and unsafe subsets. */ + List *remote_conds; + List *local_conds; + + /* Bitmap of attr numbers we need to fetch from the remote server. */ + Bitmapset *attrs_used; + + /* Cost and selectivity of local_conds. */ + QualCost local_conds_cost; + Selectivity local_conds_sel; + + /* Estimated size and cost for a scan with baserestrictinfo quals. */ + double rows; + int width; + Cost startup_cost; + Cost total_cost; + + /* Options extracted from catalogs. */ + bool use_remote_estimate; + Cost fdw_startup_cost; + Cost fdw_tuple_cost; + + /* Cached catalog information. */ + ForeignTable *table; + ForeignServer *server; + Oid umid; + + /* Join information */ + RelOptInfo *outerrel; + RelOptInfo *innerrel; + JoinType jointype; + List *joinclauses; + List *otherclauses; +} PgFdwRelationInfo; + /* in postgres_fdw.c */ extern int set_transmission_modes(void); extern void reset_transmission_modes(int nestlevel); @@ -51,13 +100,32 @@ extern void deparseSelectSql(StringInfo buf, PlannerInfo *root, RelOptInfo *baserel, Bitmapset *attrs_used, - List **retrieved_attrs); -extern void appendWhereClause(StringInfo buf, + List *remote_conds, + List **params_list, + List **fdw_scan_tlist, + List **retrieved_attrs, + StringInfo relations, + bool alias); +extern void appendConditions(StringInfo buf, PlannerInfo *root, RelOptInfo *baserel, + List *outertlist, + List *innertlist, List *exprs, - bool is_first, + const char *prefix, List **params); +extern void deparseJoinSql(StringInfo sql, + PlannerInfo *root, + RelOptInfo *baserel, + RelOptInfo *outerrel, + RelOptInfo *innerrel, + const char *sql_o, + const char *sql_i, + JoinType jointype, + List *joinclauses, + List *otherclauses, + List **fdw_scan_tlist, + List **retrieved_attrs); extern void deparseInsertSql(StringInfo buf, PlannerInfo *root, Index rtindex, Relation rel, List *targetAttrs, bool doNothing, List *returningList, diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index fcdd92e..9429d34 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -11,12 +11,17 @@ DO $d$ OPTIONS (dbname '$$||current_database()||$$', port '$$||current_setting('port')||$$' )$$; + EXECUTE $$CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (dbname '$$||current_database()||$$', + port '$$||current_setting('port')||$$' + )$$; END; $d$; CREATE USER MAPPING FOR public SERVER testserver1 OPTIONS (user 'value', password 'value'); CREATE USER MAPPING FOR CURRENT_USER SERVER loopback; +CREATE USER MAPPING FOR CURRENT_USER SERVER loopback2; -- =================================================================== -- create objects used through FDW loopback server @@ -39,6 +44,18 @@ CREATE TABLE "S 1"."T 2" ( c2 text, CONSTRAINT t2_pkey PRIMARY KEY (c1) ); +CREATE TABLE "S 1"."T 3" ( + c1 int NOT NULL, + c2 int NOT NULL, + c3 text, + CONSTRAINT t3_pkey PRIMARY KEY (c1) +); +CREATE TABLE "S 1"."T 4" ( + c1 int NOT NULL, + c2 int NOT NULL, + c4 text, + CONSTRAINT t4_pkey PRIMARY KEY (c1) +); INSERT INTO "S 1"."T 1" SELECT id, @@ -54,9 +71,23 @@ INSERT INTO "S 1"."T 2" SELECT id, 'AAA' || to_char(id, 'FM000') FROM generate_series(1, 100) id; +INSERT INTO "S 1"."T 3" + SELECT id, + id + 1, + 'AAA' || to_char(id, 'FM000') + FROM generate_series(1, 100) id; +DELETE FROM "S 1"."T 3" WHERE c1 % 2 != 0; -- delete for outer join tests +INSERT INTO "S 1"."T 4" + SELECT id, + id + 1, + 'AAA' || to_char(id, 'FM000') + FROM generate_series(1, 100) id; +DELETE FROM "S 1"."T 4" WHERE c1 % 3 != 0; -- delete for outer join tests ANALYZE "S 1"."T 1"; ANALYZE "S 1"."T 2"; +ANALYZE "S 1"."T 3"; +ANALYZE "S 1"."T 4"; -- =================================================================== -- create foreign tables @@ -87,6 +118,29 @@ CREATE FOREIGN TABLE ft2 ( ) SERVER loopback; ALTER FOREIGN TABLE ft2 DROP COLUMN cx; +CREATE FOREIGN TABLE ft4 ( + c1 int NOT NULL, + c2 int NOT NULL, + c3 text +) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 3'); + +CREATE FOREIGN TABLE ft5 ( + c1 int NOT NULL, + c2 int NOT NULL, + c3 text +) SERVER loopback OPTIONS (schema_name 'S 1', table_name 'T 4'); + +CREATE FOREIGN TABLE ft6 ( + c1 int NOT NULL, + c2 int NOT NULL, + c3 text +) SERVER loopback2 OPTIONS (schema_name 'S 1', table_name 'T 4'); +CREATE USER view_owner; +GRANT ALL ON ft5 TO view_owner; +CREATE VIEW v_ft5 AS SELECT * FROM ft5; +ALTER VIEW v_ft5 OWNER TO view_owner; +CREATE USER MAPPING FOR view_owner SERVER loopback; + -- =================================================================== -- tests for validator -- =================================================================== @@ -158,8 +212,6 @@ EXPLAIN (VERBOSE, COSTS false) SELECT * FROM ft1 t1 WHERE c1 = 102 FOR SHARE; SELECT * FROM ft1 t1 WHERE c1 = 102 FOR SHARE; -- aggregate SELECT COUNT(*) FROM ft1 t1; --- join two tables -SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; -- subquery SELECT * FROM ft1 t1 WHERE t1.c3 IN (SELECT c3 FROM ft2 t2 WHERE c1 <= 10) ORDER BY c1; -- subquery+MAX @@ -216,6 +268,86 @@ SELECT * FROM ft1 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft2 WHERE c1 < 5)); SELECT * FROM ft2 WHERE c1 = ANY (ARRAY(SELECT c1 FROM ft1 WHERE c1 < 5)); -- =================================================================== +-- JOIN queries +-- =================================================================== +-- join two tables +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +-- join three tables +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10; +SELECT t1.c1, t2.c2, t3.c3 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) JOIN ft4 t3 ON (t3.c1 = t1.c1) ORDER BY t1.c3, t1.c1 OFFSET 10 LIMIT 10; +-- left outer join +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft4 t1 LEFT JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; +-- right outer join +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft5 t1 RIGHT JOIN ft4 t2 ON (t1.c1 = t2.c1) ORDER BY t2.c1, t1.c1 OFFSET 10 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft5 t1 RIGHT JOIN ft4 t2 ON (t1.c1 = t2.c1) ORDER BY t2.c1, t1.c1 OFFSET 10 LIMIT 10; +-- full outer join +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 45 LIMIT 10; +-- full outer join + WHERE clause, only matched rows +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft4 t1 FULL JOIN ft5 t2 ON (t1.c1 = t2.c1) WHERE (t1.c1 = t2.c1 OR t1.c1 IS NULL) ORDER BY t1.c1, t2.c1 OFFSET 10 LIMIT 10; +-- join at WHERE clause +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON true WHERE (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON true WHERE (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +-- join in CTE +EXPLAIN (COSTS false, VERBOSE) +WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10; +WITH t (c1_1, c1_3, c2_1) AS (SELECT t1.c1, t1.c3, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) SELECT c1_1, c2_1 FROM t ORDER BY c1_3, c1_1 OFFSET 100 LIMIT 10; +-- ctid with whole-row reference +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +SELECT t1.ctid, t1, t2, t1.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +-- partially unsafe to push down, not pushed down +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON t2.c1 = t2.c1 JOIN ft4 t3 ON t2.c1 = t3.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10; +SELECT t1.c1 FROM ft1 t1 JOIN ft2 t2 ON t2.c1 = t2.c1 JOIN ft4 t3 ON t2.c1 = t3.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10; +-- SEMI JOIN, not pushed down +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1 FROM ft1 t1 WHERE EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c1) ORDER BY t1.c1 OFFSET 100 LIMIT 10; +-- ANTI JOIN, not pushed down +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1 FROM ft1 t1 WHERE NOT EXISTS (SELECT 1 FROM ft2 t2 WHERE t1.c1 = t2.c2) ORDER BY t1.c1 OFFSET 100 LIMIT 10; +-- CROSS JOIN, not pushed down +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft1 t1 CROSS JOIN ft2 t2 ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; +-- different server +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN ft6 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; +-- different effective user for permission check +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN v_ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft5 t1 JOIN v_ft5 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1, t2.c1 OFFSET 100 LIMIT 10; +-- unsafe join conditions +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c8 = t2.c8) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c8 = t2.c8) ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +-- local filter (unsafe conditions on one side) +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = 'foo' ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) WHERE t1.c8 = 'foo' ORDER BY t1.c3, t1.c1 OFFSET 100 LIMIT 10; +-- Aggregate after UNION, for testing setrefs +EXPLAIN (COSTS false, VERBOSE) +SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10; +SELECT t1c1, avg(t1c1 + t2c1) FROM (SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1) UNION SELECT t1.c1, t2.c1 FROM ft1 t1 JOIN ft2 t2 ON (t1.c1 = t2.c1)) AS t (t1c1, t2c1) GROUP BY t1c1 ORDER BY t1c1 OFFSET 100 LIMIT 10; +-- join two foreign tables and two local tables +EXPLAIN (COSTS false, VERBOSE) +SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON t1.c1 = t2.c1 JOIN "S 1"."T 1" t3 ON t1.c1 = t3."C 1" JOIN "S 1"."T 2" t4 ON t1.c1 = t4.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10; +SELECT t1.c1, t2.c1 FROM ft1 t1 LEFT JOIN ft2 t2 ON t1.c1 = t2.c1 JOIN "S 1"."T 1" t3 ON t1.c1 = t3."C 1" JOIN "S 1"."T 2" t4 ON t1.c1 = t4.c1 ORDER BY t1.c1 OFFSET 10 LIMIT 10; + +-- =================================================================== -- parameterized queries -- =================================================================== -- simple join @@ -834,3 +966,7 @@ DROP TYPE "Colors" CASCADE; IMPORT FOREIGN SCHEMA import_source LIMIT TO (t5) FROM SERVER loopback INTO import_dest5; -- ERROR ROLLBACK; + +-- Cleanup +DROP OWNED BY view_owner; +DROP USER view_owner;