diff options
-rw-r--r-- | contrib/Makefile | 1 | ||||
-rw-r--r-- | contrib/README | 3 | ||||
-rw-r--r-- | contrib/postgresql_fdw/.gitignore | 3 | ||||
-rw-r--r-- | contrib/postgresql_fdw/Makefile | 23 | ||||
-rw-r--r-- | contrib/postgresql_fdw/connection.c | 356 | ||||
-rw-r--r-- | contrib/postgresql_fdw/connection.h | 25 | ||||
-rw-r--r-- | contrib/postgresql_fdw/deparse.c | 364 | ||||
-rw-r--r-- | contrib/postgresql_fdw/deparse.h | 18 | ||||
-rw-r--r-- | contrib/postgresql_fdw/expected/postgresql_fdw.out | 154 | ||||
-rw-r--r-- | contrib/postgresql_fdw/postgresql_fdw--1.0.sql | 11 | ||||
-rw-r--r-- | contrib/postgresql_fdw/postgresql_fdw.c | 366 | ||||
-rw-r--r-- | contrib/postgresql_fdw/postgresql_fdw.control | 5 | ||||
-rw-r--r-- | contrib/postgresql_fdw/sql/postgresql_fdw.sql | 88 | ||||
-rw-r--r-- | doc/src/sgml/contrib.sgml | 1 | ||||
-rw-r--r-- | doc/src/sgml/filelist.sgml | 1 | ||||
-rw-r--r-- | doc/src/sgml/postgresql-fdw.sgml | 346 | ||||
-rw-r--r-- | src/backend/foreign/foreign.c | 69 | ||||
-rw-r--r-- | src/include/foreign/foreign.h | 1 |
18 files changed, 1811 insertions, 24 deletions
diff --git a/contrib/Makefile b/contrib/Makefile index 0c238aae16..6daf8684ac 100644 --- a/contrib/Makefile +++ b/contrib/Makefile @@ -42,6 +42,7 @@ SUBDIRS = \ pgcrypto \ pgrowlocks \ pgstattuple \ + postgresql_fdw \ seg \ spi \ tablefunc \ diff --git a/contrib/README b/contrib/README index a1d42a11cb..32b95e3d31 100644 --- a/contrib/README +++ b/contrib/README @@ -163,6 +163,9 @@ pgstattuple - space within a table by Tatsuo Ishii <[email protected]> +postgresql_fdw - + Foreign-data wrapper for external PostgreSQL server. + seg - Confidence-interval datatype (GiST indexing example) by Gene Selkov, Jr. <[email protected]> diff --git a/contrib/postgresql_fdw/.gitignore b/contrib/postgresql_fdw/.gitignore new file mode 100644 index 0000000000..4a8bf1781d --- /dev/null +++ b/contrib/postgresql_fdw/.gitignore @@ -0,0 +1,3 @@ +/postgresql_fdw.sql +# Generated subdirectories +/results/ diff --git a/contrib/postgresql_fdw/Makefile b/contrib/postgresql_fdw/Makefile new file mode 100644 index 0000000000..22dffc42dd --- /dev/null +++ b/contrib/postgresql_fdw/Makefile @@ -0,0 +1,23 @@ +# contrib/postgresql_fdw/Makefile + +MODULE_big = postgresql_fdw +PG_CPPFLAGS = -I$(libpq_srcdir) +OBJS = postgresql_fdw.o connection.o deparse.o +SHLIB_LINK = $(libpq) +SHLIB_PREREQS = submake-libpq + +EXTENSION = postgresql_fdw +DATA = postgresql_fdw--1.0.sql + +REGRESS = postgresql_fdw + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = contrib/postgresql_fdw +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/contrib/postgresql_fdw/connection.c b/contrib/postgresql_fdw/connection.c new file mode 100644 index 0000000000..f12b798ad9 --- /dev/null +++ b/contrib/postgresql_fdw/connection.c @@ -0,0 +1,356 @@ +/*------------------------------------------------------------------------- + * + * connectoin.c + * Connection management for postgresql_fdw + * + * Portions Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * IDENTIFICATION + * contrib/postgresql_fdw/connection.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "foreign/foreign.h" +#include "libpq-fe.h" +#include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/resowner.h" + +#include "connection.h" + +/* ============================================================================ + * Connection management functions + * ==========================================================================*/ + +/* + * Connection cache entry managed with hash table. + */ +typedef struct ConnCacheEntry +{ + /* hash key must be first */ + char name[NAMEDATALEN]; /* connection name; used as hash key */ + int refs; /* reference counter */ + PGconn *conn; /* foreign server connection */ +} ConnCacheEntry; + +/* + * Hash table which is used to cache connection to PostgreSQL servers, will be + * initialized before first attempt to connect PostgreSQL server by the backend. + */ +static HTAB *FSConnectionHash; + +/* ---------------------------------------------------------------------------- + * prototype of private functions + * --------------------------------------------------------------------------*/ +static void +cleanup_connection(ResourceReleasePhase phase, + bool isCommit, + bool isTopLevel, + void *arg); +static PGconn *connect_pg_server(ForeignServer *server, UserMapping *user); +/* + * Get a PGconn which can be used to execute foreign query on the remote + * PostgreSQL server with the user's authorization. If this was the first + * request for the server, new connection is established. + */ +PGconn * +GetConnection(ForeignServer *server, UserMapping *user) +{ + const char *conname = server->servername; + bool found; + ConnCacheEntry *entry; + PGconn *conn = NULL; + + /* initialize connection cache if it isn't */ + if (FSConnectionHash == NULL) + { + HASHCTL ctl; + + /* hash key is the name of the connection */ + MemSet(&ctl, 0, sizeof(ctl)); + ctl.keysize = NAMEDATALEN; + ctl.entrysize = sizeof(ConnCacheEntry); + /* allocate FSConnectionHash in the cache context */ + ctl.hcxt = CacheMemoryContext; + FSConnectionHash = hash_create("Foreign Connections", 32, + &ctl, + HASH_ELEM | HASH_CONTEXT); + } + + /* Is there any cached and valid connection with such name? */ + entry = hash_search(FSConnectionHash, conname, HASH_ENTER, &found); + if (found) + { + if (entry->conn != NULL) + { + entry->refs++; + elog(DEBUG3, "ref %d for %s", entry->refs, entry->name); + return entry->conn; + } + + /* + * Connection cache entry was found but connection in it is invalid. + * We reuse entry to store newly established connection later. + */ + } + else + { + /* + * Use ResourceOner to clean the connection up on error including + * user interrupt. + */ + entry->refs = 0; + entry->conn = NULL; + RegisterResourceReleaseCallback(cleanup_connection, entry); + } + + /* + * Here we have to establish new connection. + * Use PG_TRY block to ensure closing connection on error. + */ + PG_TRY(); + { + /* Connect to the foreign PostgreSQL server */ + conn = connect_pg_server(server, user); + + /* + * Initialize the cache entry to keep new connection. + * Note: entry->name has been initialized in hash_search(HASH_ENTER). + */ + entry->refs = 1; + entry->conn = conn; + elog(DEBUG3, "connected to %s (%d)", entry->name, entry->refs); + } + PG_CATCH(); + { + PQfinish(conn); + entry->refs = 0; + entry->conn = NULL; + PG_RE_THROW(); + } + PG_END_TRY(); + + return conn; +} + +/* + * For non-superusers, insist that the connstr specify a password. This + * prevents a password from being picked up from .pgpass, a service file, + * the environment, etc. We don't want the postgres user's passwords + * to be accessible to non-superusers. + */ +static void +check_conn_params(const char **keywords, const char **values) +{ + int i; + + /* no check required if superuser */ + if (superuser()) + return; + + /* ok if params contain a non-empty password */ + for (i = 0; keywords[i] != NULL; i++) + { + if (strcmp(keywords[i], "password") == 0 && values[i][0] != '\0') + return; + } + + ereport(ERROR, + (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED), + errmsg("password is required"), + errdetail("Non-superusers must provide a password in the connection string."))); +} + +static int +flatten_generic_options(List *defelems, const char **keywords, const char **values) +{ + ListCell *lc; + int i; + + i = 0; + foreach(lc, defelems) + { + DefElem *d = (DefElem *) lfirst(lc); + keywords[i] = d->defname; + values[i] = strVal(d->arg); + i++; + } + return i; +} + +static PGconn * +connect_pg_server(ForeignServer *server, UserMapping *user) +{ + const char *conname = server->servername; + PGconn *conn; + const char **all_keywords; + const char **all_values; + const char **keywords; + const char **values; + int n; + int i, j; + + /* + * Construct connection params from generic options of ForeignServer and + * UserMapping. Generic options might not be a one of connection options. + */ + n = list_length(server->options) + list_length(user->options) + 1; + all_keywords = (const char **) palloc(sizeof(char *) * n); + all_values = (const char **) palloc(sizeof(char *) * n); + keywords = (const char **) palloc(sizeof(char *) * n); + values = (const char **) palloc(sizeof(char *) * n); + n = 0; + n += flatten_generic_options(server->options, + all_keywords + n, all_values + n); + n += flatten_generic_options(user->options, + all_keywords + n, all_values + n); + all_keywords[n] = all_values[n] = NULL; + + for (i = 0, j = 0; all_keywords[i]; i++) + { + /* Use only valid libpq connection options. */ + if (!is_conninfo_option(all_keywords[i])) + continue; + + keywords[j] = all_keywords[i]; + values[j] = all_values[i]; + j++; + } + keywords[j] = values[j] = NULL; + pfree(all_keywords); + pfree(all_values); + + /* TODO: omit newly added parameters when connecting to older server. */ + + /* verify connection parameters and do connect */ + check_conn_params(keywords, values); + conn = PQconnectdbParams(keywords, values, 0); + if (!conn || PQstatus(conn) != CONNECTION_OK) + ereport(ERROR, + (errcode(ERRCODE_SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION), + errmsg("could not connect to server \"%s\"", conname), + errdetail("%s", PQerrorMessage(conn)))); + pfree(keywords); + pfree(values); + + /* + * Check that non-superuser has used password to establish connection. + * This check logic is based on dblink_security_check() in contrib/dblink. + * + * XXX Should we check this even if we don't provide unsafe version like + * dblink_connect_u()? + */ + if (!superuser() && !PQconnectionUsedPassword(conn)) + { + PQfinish(conn); + ereport(ERROR, + (errcode(ERRCODE_S_R_E_PROHIBITED_SQL_STATEMENT_ATTEMPTED), + errmsg("password is required"), + errdetail("Non-superuser cannot connect if the server does not request a password."), + errhint("Target server's authentication method must be changed."))); + } + + /* + * Set client_encoding of the connection so that libpq can convert encoding + * properly. + */ + PQsetClientEncoding(conn, GetDatabaseEncodingName()); + + return conn; +} + +/* + * Mark the connection as "unused", and close it if the caller was the last + * user of the connection. + */ +void +ReleaseConnection(PGconn *conn) +{ + HASH_SEQ_STATUS scan; + ConnCacheEntry *entry; + + if (conn == NULL) + return; + + /* + * We need to scan seqencially since we use the address to find appropriate + * PGconn from the hash table. + */ + hash_seq_init(&scan, FSConnectionHash); + while ((entry = (ConnCacheEntry *) hash_seq_search(&scan))) + { + if (entry->conn == conn) + break; + } + hash_seq_term(&scan); + + /* + * If the released connection was an orphan, just close it. + */ + if (entry == NULL) + { + PQfinish(conn); + return; + } + + /* If the caller was the last referer, unregister it from cache. */ + entry->refs--; + elog(DEBUG3, "ref %d for %s", entry->refs, entry->name); + if (entry->refs == 0) + { + elog(DEBUG3, "closing connection \"%s\"", entry->name); + PQfinish(entry->conn); + entry->refs = 0; + entry->conn = NULL; + } +} + +/* + * Clean the connection up via ResourceOwner. + */ +static void +cleanup_connection(ResourceReleasePhase phase, + bool isCommit, + bool isTopLevel, + void *arg) +{ + ConnCacheEntry *entry = (ConnCacheEntry *) arg; + + /* If the transaction was committed, don't close connections. */ + if (isCommit) + return; + + /* + * We clean the connection up on post-lock because foreign connections are + * backend-internal resource. + */ + if (phase != RESOURCE_RELEASE_AFTER_LOCKS) + return; + + /* + * We ignore cleanup for ResourceOwners other than transaction. At this + * point, such a ResourceOwner is only Portal. + */ + if (CurrentResourceOwner != CurTransactionResourceOwner) + return; + + /* + * We don't care whether we are in TopTransaction or Subtransaction. + * Anyway, we close the connection and reset the reference counter. + */ + if (entry->conn != NULL) + { + elog(DEBUG3, "closing connection to %s", entry->name); + PQfinish(entry->conn); + entry->refs = 0; + entry->conn = NULL; + } + else + elog(DEBUG3, "connection to %s already closed", entry->name); +} + + diff --git a/contrib/postgresql_fdw/connection.h b/contrib/postgresql_fdw/connection.h new file mode 100644 index 0000000000..1497d17724 --- /dev/null +++ b/contrib/postgresql_fdw/connection.h @@ -0,0 +1,25 @@ +/*------------------------------------------------------------------------- + * + * connectoin.h + * Connection management for postgresql_fdw + * + * Portions Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * IDENTIFICATION + * contrib/postgresql_fdw/connection.h + * + *------------------------------------------------------------------------- + */ +#ifndef CONNECTION_H +#define CONNECTION_H + +#include "foreign/foreign.h" +#include "libpq-fe.h" + +/* + * Connection management + */ +PGconn *GetConnection(ForeignServer *server, UserMapping *user); +void ReleaseConnection(PGconn *conn); + +#endif /* CONNECTION_H */ diff --git a/contrib/postgresql_fdw/deparse.c b/contrib/postgresql_fdw/deparse.c new file mode 100644 index 0000000000..3303fde4aa --- /dev/null +++ b/contrib/postgresql_fdw/deparse.c @@ -0,0 +1,364 @@ +/*------------------------------------------------------------------------- + * + * deparse.c + * Utility for deparsing parse-tree into SQL statement. + * + * Portions Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * IDENTIFICATION + * contrib/postgresql_fdw/deparse.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/pg_namespace.h" +#include "foreign/foreign.h" +#include "lib/stringinfo.h" +#include "nodes/nodeFuncs.h" +#include "nodes/relation.h" +#include "optimizer/clauses.h" +#include "optimizer/var.h" +#include "parser/parsetree.h" +#include "utils/builtins.h" +#include "utils/lsyscache.h" + +#include "deparse.h" + +/* helper for deparsing a request into SQL statement */ +static bool is_foreign_qual(PlannerInfo *root, RelOptInfo *baserel, Expr *expr); +static bool foreign_qual_walker(Node *node, void *context); + +/* + * Deparse a var into column name, result is quoted if necessary. + */ +static void +deparse_var(PlannerInfo *root, StringInfo buf, Var *var) +{ + RangeTblEntry *rte; + char *attname; + + if (var->varlevelsup != 0) + elog(ERROR, "unexpected varlevelsup %d in remote query", + var->varlevelsup); + + if (var->varno < 1 || var->varno > list_length(root->parse->rtable)) + elog(ERROR, "unexpected varno %d in remote query", var->varno); + rte = rt_fetch(var->varno, root->parse->rtable); + + attname = get_rte_attribute_name(rte, var->varattno); + appendStringInfoString(buf, quote_identifier(attname)); +} + +typedef struct +{ + PlannerInfo *root; + RelOptInfo *foreignrel; +} remotely_executable_cxt; + +/* + * return true if procid can be executed in foreign server. + */ +static bool +is_proc_remotely_executable(Oid procid) +{ + /* + * assume that only built-in functions can be pushed down. + * TODO routine mapping should be used to determine whether the function + * can be pushed down. + */ + if (get_func_namespace(procid) != PG_CATALOG_NAMESPACE) + return false; + /* we don't check volatility here, that's done once at the top-level */ + + return true; +} + +/* + * return true if node can NOT be evaluatated in foreign server. + * + * An expression which consists of expressions below can be evaluated in + * the foreign server. + * - constant value + * - variable (foreign table column) + * - external parameter (parameter of prepared statement) + * - array + * - bool expression (AND/OR/NOT) + * - NULL test (IS [NOT] NULL) + * - operator + * - IMMUTABLE or STABLE only + * - It is required that the meaning of the operator be the same as the + * local server in the foreign server. + * - function + * - IMMUTABLE or STABLE only + * - It is required that the meaning of the operator be the same as the + * local server in the foreign server. + * - scalar array operator (ANY/ALL) + */ +static bool +foreign_qual_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + + switch (nodeTag(node)) + { + case T_Const: + case T_ArrayExpr: + case T_BoolExpr: + case T_NullTest: + case T_DistinctExpr: + case T_ScalarArrayOpExpr: + /* + * These type of nodes are known as safe to be pushed down. + * Of course the subtree of the node, if any, should be checked + * continuously. + */ + break; + case T_Param: + /* + * External parameter can be pushed down. + * TODO: Pass internal parameters to the foreign server. It needs + * index renumbering. + */ + { + if (((Param *) node)->paramkind != PARAM_EXTERN) + return true; + } + break; + case T_OpExpr: + { + OpExpr *op = (OpExpr *) node; + + if (!is_proc_remotely_executable(op->opfuncid)) + return true; + /* arguments are checked later via walker */ + } + break; + case T_FuncExpr: + { + FuncExpr *fe = (FuncExpr *) node; + + if (!is_proc_remotely_executable(fe->funcid)) + return true; + /* arguments are checked later via walker */ + } + break; + case T_Var: + { + /* + * Var can be pushed down if it is in the foreign table. + * XXX Var of other relation can be here? + */ + Var *var = (Var *) node; + remotely_executable_cxt *r_context; + + r_context = (remotely_executable_cxt *) context; + if (var->varno != r_context->foreignrel->relid || + var->varlevelsup != 0) + return true; + } + break; + default: + /* Other expression can't be pushed down */ + elog(DEBUG3, "node is too complex"); + elog(DEBUG3, "%s", nodeToString(node)); + return true; + } + + return expression_tree_walker(node, foreign_qual_walker, context); +} + + +/* + * Check whether the ExprState node can be evaluated in foreign server. + * + * Actual check is implemented in foreign_qual_walker. + */ +static bool +is_foreign_qual(PlannerInfo *root, RelOptInfo *baserel, Expr *expr) +{ + remotely_executable_cxt context; + + context.root = root; + context.foreignrel = baserel; + + /* + * Check that the expression consists of nodes which are known as safe to + * be pushed down. + */ + if (foreign_qual_walker((Node *) expr, &context)) + return false; + + /* Check that the expression doesn't include any volatile function. */ + if (contain_volatile_functions((Node *) expr)) + return false; + + return true; +} + +/* + * Deparse query request into SQL statement. + * + * If an expression in PlanState.qual list satisfies is_foreign_qual(), the + * expression is: + * - deparsed into WHERE clause of remote SQL statement to evaluate that + * expression on remote side + * - removed from PlanState.qual list to avoid duplicate evaluation, on + * remote side and local side + */ +char * +deparseSql(Oid foreigntableid, PlannerInfo *root, RelOptInfo *baserel) +{ + AttrNumber attr; + List *attr_used = NIL; + List *context; + List *foreign_expr = NIL; + StringInfoData sql; + ForeignTable *table = GetForeignTable(foreigntableid); + ListCell *lc; + bool first; + char *nspname = NULL; + char *relname = NULL; + + /* extract ForeignScan and RangeTblEntry */ + + /* prepare to deparse plan */ + initStringInfo(&sql); + + context = deparse_context_for("foreigntable", foreigntableid); + + /* + * Determine which qual can be pushed down. + * + * The expressions which satisfy is_foreign_qual() are deparsed into WHERE + * clause of result SQL string, and they could be removed from qual of + * PlanState to avoid duplicate evaluation at ExecScan(). + * + * We never change the qual in the Plan node which was made by PREPARE + * statement to make following EXECUTE statements work properly. The Plan + * node is used repeatedly to create PlanState for each EXECUTE statement. + * + * We do this before deparsing SELECT clause because attributes which + * aren't used in neither reltargetlist nor baserel->baserestrictinfo, + * quals evaluated on local, can be replaced with NULL in the SELECT + * clause. + */ + if (baserel->baserestrictinfo) + { + List *local_qual = NIL; + ListCell *lc; + + /* + * Divide qual of PlanState into two lists, one for local evaluation + * and one for foreign evaluation. + */ + foreach (lc, baserel->baserestrictinfo) + { + RestrictInfo *ri = (RestrictInfo *) lfirst(lc); + + if (is_foreign_qual(root, baserel, ri->clause)) + { + /* XXX: deparse and add to sql here */ + foreign_expr = lappend(foreign_expr, ri->clause); + } + else + local_qual = lappend(local_qual, ri); + } + /* + * XXX: If the remote side is not reliable enough, we can keep the qual + * in PlanState as is and evaluate them on local side too. If so, just + * omit replacement below. + */ + baserel->baserestrictinfo = local_qual; + + } + + /* Collect used columns from restrict information and target list */ + attr_used = list_union(attr_used, baserel->reltargetlist); + foreach (lc, baserel->baserestrictinfo) + { + List *l; + RestrictInfo *ri = lfirst(lc); + + l = pull_var_clause((Node *) ri->clause, PVC_RECURSE_PLACEHOLDERS); + attr_used = list_union(attr_used, l); + } + + /* deparse SELECT target list */ + appendStringInfoString(&sql, "SELECT "); + first = true; + for (attr = 1; attr <= baserel->max_attr; attr++) + { + Var *var; + + if (!first) + appendStringInfoString(&sql, ", "); + first = false; + + /* Use "NULL" for unused columns. */ + foreach (lc, attr_used) + { + var = lfirst(lc); + if (var->varattno == attr) + break; + var = NULL; + } + + if (var != NULL) + deparse_var(root, &sql, var); + else + appendStringInfo(&sql, "NULL"); + } + + /* + * Deparse FROM + * + * If the foreign table has generic option "nspname" and/or "relname", use + * them in the foreign query. Otherwise, use local catalog names. + */ + foreach(lc, table->options) + { + DefElem *opt = lfirst(lc); + if (strcmp(opt->defname, "nspname") == 0) + nspname = pstrdup(strVal(opt->arg)); + else if (strcmp(opt->defname, "relname") == 0) + relname = pstrdup(strVal(opt->arg)); + } + if (nspname == NULL) + nspname = get_namespace_name(get_rel_namespace(foreigntableid)); + if (relname == NULL) + relname = get_rel_name(foreigntableid); + appendStringInfo(&sql, " FROM %s.%s", + quote_identifier(nspname), + quote_identifier(relname)); + + /* + * deparse WHERE if some quals can be pushed down + */ + if (foreign_expr != NIL) + { + /* + * Deparse quals to be evaluated in the foreign server if any. + * TODO: modify deparse_expression() to deparse conditions which use + * internal parameters. + */ + if (foreign_expr != NIL) + { + Node *node; + node = (Node *) make_ands_explicit(foreign_expr); + appendStringInfo(&sql, " WHERE %s", + deparse_expression(node, context, false, false)); + /* + * The contents of the list MUST NOT be free-ed because they are + * referenced from Plan.qual list. + */ + list_free(foreign_expr); + } + } + + elog(DEBUG1, "deparsed SQL is \"%s\"", sql.data); + + return sql.data; +} + diff --git a/contrib/postgresql_fdw/deparse.h b/contrib/postgresql_fdw/deparse.h new file mode 100644 index 0000000000..bf1b018243 --- /dev/null +++ b/contrib/postgresql_fdw/deparse.h @@ -0,0 +1,18 @@ +/* + * deparse.h + * + * contrib/postgresql_fdw/deparse.h + * Copyright (c) 2011, PostgreSQL Global Development Group + * ALL RIGHTS RESERVED; + * + */ + +#ifndef DEPARSE_H +#define DEPARSE_H + +/* + * deparse.c: deparsing query-tree into SQL statement + */ +char *deparseSql(Oid foreigntableid, PlannerInfo *root, RelOptInfo *baserel); + +#endif /* DEPARSE_H */ diff --git a/contrib/postgresql_fdw/expected/postgresql_fdw.out b/contrib/postgresql_fdw/expected/postgresql_fdw.out new file mode 100644 index 0000000000..dea1ca1a40 --- /dev/null +++ b/contrib/postgresql_fdw/expected/postgresql_fdw.out @@ -0,0 +1,154 @@ +SET DATESTYLE = 'Postgres, MDY'; +-- ============================================================================= +-- Prepare section +-- ============================================================================= +CREATE EXTENSION postgresql_fdw; +-- connect database for regression test +\c contrib_regression +-- define fdw-related objects +CREATE SERVER loopback1 FOREIGN DATA WRAPPER postgresql_fdw + OPTIONS (dbname 'contrib_regression'); +CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgresql_fdw + OPTIONS (dbname 'contrib_regression'); +CREATE USER MAPPING FOR PUBLIC SERVER loopback1; +CREATE USER MAPPING FOR PUBLIC SERVER loopback2 OPTIONS (user 'invalid'); +CREATE TABLE t1( + c1 integer, + c2 text, + c3 date +); +COPY t1 FROM stdin; +CREATE TABLE t2( + c1 integer, + c2 text, + c3 date +); +COPY t2 FROM stdin; +CREATE FOREIGN TABLE ft1 ( + c1 integer, + c2 text, + c3 date +) SERVER loopback1 OPTIONS (relname 't1'); +CREATE FOREIGN TABLE ft2 ( + c1 integer, + c2 text, + c3 date +) SERVER loopback2 OPTIONS (relname 'invalid'); +-- simple query and connection caching +SELECT ft1.* FROM ft1 ORDER BY c1; + c1 | c2 | c3 +----+-----+------------ + 1 | foo | 01-01-1970 + 2 | bar | 01-02-1970 + 3 | buz | 01-03-1970 +(3 rows) + +SELECT * FROM ft2 ORDER BY c1; -- ERROR +ERROR: could not connect to server "loopback2" +DETAIL: FATAL: role "invalid" does not exist + +ALTER USER MAPPING FOR PUBLIC SERVER loopback2 OPTIONS (DROP user); +SELECT * FROM ft2 ORDER BY c1; -- ERROR +ERROR: could not execute foreign query +DETAIL: ERROR: relation "public.invalid" does not exist +LINE 1: SELECT c1, c2, c3 FROM public.invalid + ^ + +HINT: SELECT c1, c2, c3 FROM public.invalid +ALTER FOREIGN TABLE ft2 OPTIONS (SET relname 't2'); +SELECT * FROM ft2 ORDER BY c1; + c1 | c2 | c3 +----+-----+------------ + 1 | foo | 01-01-1970 + 12 | bar | 01-02-1970 + 13 | buz | 01-03-1970 +(3 rows) + +-- subquery +SELECT c1 FROM t1 WHERE c1 = (SELECT c1 FROM ft1 ORDER BY c1 LIMIT 1); + c1 +---- + 1 +(1 row) + +SELECT c1 FROM ft1 WHERE c1 = (SELECT c1 FROM t1 ORDER BY c1 LIMIT 1); + c1 +---- + 1 +(1 row) + +-- query with projection +SELECT c1 FROM ft1 ORDER BY c1; + c1 +---- + 1 + 2 + 3 +(3 rows) + +-- join two foreign tables +SELECT * FROM ft1 JOIN ft2 ON (ft1.c1 = ft2.c1) ORDER BY ft1.c1; + c1 | c2 | c3 | c1 | c2 | c3 +----+-----+------------+----+-----+------------ + 1 | foo | 01-01-1970 | 1 | foo | 01-01-1970 +(1 row) + +-- join itself +SELECT * FROM ft1 t1 JOIN ft1 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1; + c1 | c2 | c3 | c1 | c2 | c3 +----+-----+------------+----+-----+------------ + 1 | foo | 01-01-1970 | 1 | foo | 01-01-1970 + 2 | bar | 01-02-1970 | 2 | bar | 01-02-1970 + 3 | buz | 01-03-1970 | 3 | buz | 01-03-1970 +(3 rows) + +-- outer join +SELECT * FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY 1,2,3,4,5,6; + c1 | c2 | c3 | c1 | c2 | c3 +----+-----+------------+----+-----+------------ + 1 | foo | 01-01-1970 | 1 | foo | 01-01-1970 + 2 | bar | 01-02-1970 | | | + 3 | buz | 01-03-1970 | | | +(3 rows) + +-- WHERE clause push-down +SELECT * FROM ft1 WHERE c1 = 1 AND c2 = lower('FOO') AND c3 < now() and c3 < clock_timestamp(); + c1 | c2 | c3 +----+-----+------------ + 1 | foo | 01-01-1970 +(1 row) + +EXPLAIN (COSTS FALSE) SELECT * FROM ft1 WHERE c1 = 1 AND c2 = lower('FOO') AND c3 < now() and c3 < clock_timestamp(); + QUERY PLAN +--------------------------------------------------------------------------------------------------------- + Foreign Scan on ft1 + Filter: (c3 < clock_timestamp()) + Remote SQL: SELECT c1, c2, c3 FROM public.t1 WHERE ((c3 < now()) AND (c1 = 1) AND (c2 = 'foo'::text)) +(3 rows) + +-- prepared statement +PREPARE st(int) AS SELECT * FROM ft1 WHERE c1 > $1 ORDER BY c1; +EXECUTE st(1); + c1 | c2 | c3 +----+-----+------------ + 2 | bar | 01-02-1970 + 3 | buz | 01-03-1970 +(2 rows) + +EXECUTE st(2); + c1 | c2 | c3 +----+-----+------------ + 3 | buz | 01-03-1970 +(1 row) + +DEALLOCATE st; +-- clean up +DROP TABLE t1 CASCADE; +DROP EXTENSION postgresql_fdw CASCADE; +NOTICE: drop cascades to 6 other objects +DETAIL: drop cascades to server loopback1 +drop cascades to user mapping for public +drop cascades to foreign table ft1 +drop cascades to server loopback2 +drop cascades to user mapping for public +drop cascades to foreign table ft2 diff --git a/contrib/postgresql_fdw/postgresql_fdw--1.0.sql b/contrib/postgresql_fdw/postgresql_fdw--1.0.sql new file mode 100644 index 0000000000..37facd424e --- /dev/null +++ b/contrib/postgresql_fdw/postgresql_fdw--1.0.sql @@ -0,0 +1,11 @@ +/* contrib/postgresql_fdw/postgresql_fdw--1.0.sql */ + +CREATE OR REPLACE FUNCTION postgresql_fdw_handler () +RETURNS fdw_handler +AS 'MODULE_PATHNAME','postgresql_fdw_handler' +LANGUAGE C STRICT; + +CREATE FOREIGN DATA WRAPPER postgresql_fdw +VALIDATOR postgresql_fdw_validator +HANDLER postgresql_fdw_handler; + diff --git a/contrib/postgresql_fdw/postgresql_fdw.c b/contrib/postgresql_fdw/postgresql_fdw.c new file mode 100644 index 0000000000..9fe7348f02 --- /dev/null +++ b/contrib/postgresql_fdw/postgresql_fdw.c @@ -0,0 +1,366 @@ +/*------------------------------------------------------------------------- + * + * postgresql_fdw.c + * foreign-data wrapper for PostgreSQL + * + * Portions Copyright (c) 2010-2011, PostgreSQL Global Development Group + * + * IDENTIFICATION + * contrib/postgresql_fdw/postgresql_fdw.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "commands/explain.h" +#include "foreign/fdwapi.h" +#include "foreign/foreign.h" +#include "funcapi.h" +#include "libpq-fe.h" +#include "miscadmin.h" +#include "optimizer/plancat.h" +#include "parser/parsetree.h" +#include "utils/lsyscache.h" + +#include "connection.h" +#include "deparse.h" + +PG_MODULE_MAGIC; + +extern Datum postgresql_fdw_handler(PG_FUNCTION_ARGS); + +/* + * FDW routines + */ +static FdwPlan *pgPlanForeignScan(Oid foreigntableid, PlannerInfo *root, + RelOptInfo *baserel); +static void pgExplainForeignScan(ForeignScanState *node, + struct ExplainState *es); +static void pgBeginForeignScan(ForeignScanState *node, int eflags); +static TupleTableSlot *pgIterateForeignScan(ForeignScanState *node); +static void pgReScanForeignScan(ForeignScanState *node); +static void pgEndForeignScan(ForeignScanState *node); + +static void estimate_costs(PlannerInfo *root, RelOptInfo *baserel, Cost *startup_cost, Cost *total_cost); +static void storeResult(TupleTableSlot *slot, PGresult *res, int rowno); + +/* + * PostgreSQL specific portion of a foreign query request + */ +typedef struct pgFdwExecutionState +{ + PGconn *conn; /* connection used for the scan */ + PGresult *res; /* result of the scan, held until the scan ends */ + int nextrow; /* row index to be returned in next fetch */ +} pgFdwExecutionState; + + +/* + * return foreign-data wrapper handler object to execute foreign-data wrapper + * routines. + */ +PG_FUNCTION_INFO_V1(postgresql_fdw_handler); +Datum +postgresql_fdw_handler(PG_FUNCTION_ARGS) +{ + FdwRoutine *fdwroutine = makeNode(FdwRoutine); + + fdwroutine->PlanForeignScan = pgPlanForeignScan; + fdwroutine->ExplainForeignScan = pgExplainForeignScan; + fdwroutine->BeginForeignScan = pgBeginForeignScan; + fdwroutine->IterateForeignScan = pgIterateForeignScan; + fdwroutine->ReScanForeignScan = pgReScanForeignScan; + fdwroutine->EndForeignScan = pgEndForeignScan; + + PG_RETURN_POINTER(fdwroutine); +} + +/* + * pgPlanForeignScan + * Create FdwPlan for the scan + */ +static FdwPlan * +pgPlanForeignScan(Oid foreigntableid, PlannerInfo *root, RelOptInfo *baserel) +{ + FdwPlan *fdwplan; + char *sql; + + fdwplan = makeNode(FdwPlan); + + sql = deparseSql(foreigntableid, root, baserel); + + estimate_costs(root, baserel, &fdwplan->startup_cost, + &fdwplan->total_cost); + + fdwplan->fdw_private = lappend(NIL, makeString(sql)); + + return fdwplan; +} + +/* + * pgExplainForeignScan + * Produce extra output for EXPLAIN + */ +static void +pgExplainForeignScan(ForeignScanState *node, struct ExplainState *es) +{ + FdwPlan *fdwplan; + char *sql; + + fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan; + sql = strVal(list_nth(fdwplan->fdw_private, 0)); + ExplainPropertyText("Remote SQL", sql, es); +} + +/* + * Initiate actual scan on a foreign table. + */ +static void +pgBeginForeignScan(ForeignScanState *node, int eflags) +{ + pgFdwExecutionState *festate; + + Oid relid; + ForeignTable *table; + ForeignServer *server; + UserMapping *user; + + FdwPlan *fdwplan; + const char *sql; + PGconn *conn; + PGresult *res; + ParamListInfo params = node->ss.ps.state->es_param_list_info; + int numParams = params ? params->numParams : 0; + Oid *types = NULL; + const char **values = NULL; + + elog(DEBUG3, "%s() called", __FUNCTION__); + + /* + * Do nothing in EXPLAIN (no ANALYZE) case. node->fdw_state stays NULL. + */ + if (eflags & EXEC_FLAG_EXPLAIN_ONLY) + return; + + festate = palloc(sizeof(pgFdwExecutionState)); + + /* Get connection to external PostgreSQL server. */ + relid = RelationGetRelid(node->ss.ss_currentRelation); + table = GetForeignTable(relid); + server = GetForeignServer(table->serverid); + user = GetUserMapping(GetOuterUserId(), table->serverid); + conn = GetConnection(server, user); + festate->conn = conn; + + /* construct parameter array in text format */ + /* TODO: omit unused parameter */ + if (numParams > 0) + { + int i; + + types = palloc0(sizeof(Oid) * numParams); + values = palloc0(sizeof(char *) * numParams); + for (i = 0; i < numParams; i++) + { + types[i] = params->params[i].ptype; + if (params->params[i].isnull) + values[i] = NULL; + else + { + Oid out_func_oid; + bool isvarlena; + FmgrInfo func; + + /* TODO: send parameters in binary format rather than text */ + getTypeOutputInfo(types[i], &out_func_oid, &isvarlena); + fmgr_info(out_func_oid, &func); + values[i] = + OutputFunctionCall(&func, params->params[i].value); + } + } + } + + /* + * Execute query with the parameters. + * TODO: support internal parameters(PARAM_EXTERN) + * TODO: support cursor mode for huge result sets. + */ + fdwplan = ((ForeignScan *) node->ss.ps.plan)->fdwplan; + sql = strVal(list_nth(fdwplan->fdw_private, 0)); + res = PQexecParams(conn, sql, numParams, types, values, NULL, NULL, 0); + if (numParams > 0) + { + int i; + pfree(types); + for (i = 0; i < numParams; i++) + pfree((char *) values[i]); + pfree(values); + } + + /* + * If the query has failed, reporting details is enough here. + * Connections which are used by this query (including other scans) will + * be cleaned up by the foreign connection manager. + */ + if (!res || PQresultStatus(res) != PGRES_TUPLES_OK) + { + char *msg; + + msg = pstrdup(PQerrorMessage(conn)); + PQclear(res); + ereport(ERROR, + (errmsg("could not execute foreign query"), + errdetail("%s", msg), + errhint("%s", sql))); + } + + festate->res = res; + festate->nextrow = 0; + + node->fdw_state = (void *) festate; +} + +/* + * return tuples one by one. + * - execute SQL statement which was deparsed in pgBeginForeignScan() + * + * The all of result are fetched at once when pgIterateForeignScan() is called + * first after pgBeginForeignScan() or pgReScanForeignScan(). + * pgIterateForeignScan() moves the next tuple from tupstore to TupleTableSlot + * in ScanState. + */ +static TupleTableSlot * +pgIterateForeignScan(ForeignScanState *node) +{ + TupleTableSlot *slot = node->ss.ss_ScanTupleSlot; + pgFdwExecutionState *festate = (pgFdwExecutionState *) node->fdw_state; + + elog(DEBUG3, "%s() called", __FUNCTION__); + + if (festate->nextrow == PQntuples(festate->res)) + ExecClearTuple(slot); + else + storeResult(slot, festate->res, festate->nextrow++); + + return slot; +} + +/* + * Restart this scan by reseting next fetch location. + */ +static void +pgReScanForeignScan(ForeignScanState *node) +{ + ((pgFdwExecutionState *) node->fdw_state)->nextrow = 0; +} + +/* + * Release resources used for this foreign scan. + */ +static void +pgEndForeignScan(ForeignScanState *node) +{ + pgFdwExecutionState *festate = (pgFdwExecutionState *) node->fdw_state; + + if (festate == NULL) + return; + + PQclear(festate->res); + ReleaseConnection(festate->conn); + pfree(festate); +} + +/* + * Store a PGresult into tuplestore. + */ +static void +storeResult(TupleTableSlot *slot, PGresult *res, int rowno) +{ + int i; + int nfields; + int attnum; /* number of non-dropped columns */ + char **values; + AttInMetadata *attinmeta; + Form_pg_attribute *attrs; + TupleDesc tupdesc = slot->tts_tupleDescriptor; + + nfields = PQnfields(res); + attrs = tupdesc->attrs; + + /* count non-dropped columns */ + for (attnum = 0, i = 0; i < tupdesc->natts; i++) + if (!attrs[i]->attisdropped) + attnum++; + + /* check result and tuple descriptor have the same number of columns */ + if (attnum > 0 && attnum != nfields) + ereport(ERROR, + (errcode(ERRCODE_DATATYPE_MISMATCH), + errmsg("remote query result rowtype does not match " + "the specified FROM clause rowtype"), + errdetail("expected %d, actual %d", attnum, nfields))); + + /* buffer should include dropped columns */ + values = palloc(sizeof(char *) * tupdesc->natts); + + /* put all tuples into the tuplestore */ + attinmeta = TupleDescGetAttInMetadata(tupdesc); + { + int j; + HeapTuple tuple; + + for (i = 0, j = 0; i < tupdesc->natts; i++) + { + /* skip dropped columns. */ + if (attrs[i]->attisdropped) + { + values[i] = NULL; + continue; + } + + if (PQgetisnull(res, rowno, j)) + values[i] = NULL; + else + values[i] = PQgetvalue(res, rowno, j); + j++; + } + + /* + * Build the tuple and put it into the slot. + * We don't have to free the tuple explicitly because it's been + * allocated in the per-tuple context. + */ + tuple = BuildTupleFromCStrings(attinmeta, values); + ExecStoreTuple(tuple, slot, InvalidBuffer, false); + } + + pfree(values); +} + + +/* + * Estimate costs of scanning on a foreign table. + * + * baserel->baserestrictinfo can be used to examine quals on the relation. + */ +static void +estimate_costs(PlannerInfo *root, RelOptInfo *baserel, Cost *startup_cost, Cost *total_cost) +{ + RangeTblEntry *rte; + double connection_cost = 0.0; + double transfer_cost = 0.0; + + elog(DEBUG3, "%s() called", __FUNCTION__); + + /* get avarage width estimation */ + rte = planner_rt_fetch(baserel->relid, root); + baserel->width = get_relation_data_width(rte->relid, baserel->attr_widths); + + /* XXX arbitrary guesses */ + connection_cost = 10.0; + transfer_cost = 1.0; + *startup_cost += connection_cost; + *total_cost += connection_cost; + *total_cost += transfer_cost * baserel->width * baserel->tuples; +} + diff --git a/contrib/postgresql_fdw/postgresql_fdw.control b/contrib/postgresql_fdw/postgresql_fdw.control new file mode 100644 index 0000000000..ef78aafefd --- /dev/null +++ b/contrib/postgresql_fdw/postgresql_fdw.control @@ -0,0 +1,5 @@ +# postgresql_fdw extension +comment = 'Foreign-data wrapepr for external PostgreSQL server' +default_version = '1.0' +module_pathname = '$libdir/postgresql_fdw' +relocatable = true diff --git a/contrib/postgresql_fdw/sql/postgresql_fdw.sql b/contrib/postgresql_fdw/sql/postgresql_fdw.sql new file mode 100644 index 0000000000..b75f09d846 --- /dev/null +++ b/contrib/postgresql_fdw/sql/postgresql_fdw.sql @@ -0,0 +1,88 @@ +SET DATESTYLE = 'Postgres, MDY'; + +-- ============================================================================= +-- Prepare section +-- ============================================================================= +CREATE EXTENSION postgresql_fdw; + +-- connect database for regression test +\c contrib_regression + +-- define fdw-related objects +CREATE SERVER loopback1 FOREIGN DATA WRAPPER postgresql_fdw + OPTIONS (dbname 'contrib_regression'); +CREATE SERVER loopback2 FOREIGN DATA WRAPPER postgresql_fdw + OPTIONS (dbname 'contrib_regression'); + +CREATE USER MAPPING FOR PUBLIC SERVER loopback1; +CREATE USER MAPPING FOR PUBLIC SERVER loopback2 OPTIONS (user 'invalid'); + +CREATE TABLE t1( + c1 integer, + c2 text, + c3 date +); + +COPY t1 FROM stdin; +1 foo 1970-01-01 +2 bar 1970-01-02 +3 buz 1970-01-03 +\. + +CREATE TABLE t2( + c1 integer, + c2 text, + c3 date +); + +COPY t2 FROM stdin; +1 foo 1970-01-01 +12 bar 1970-01-02 +13 buz 1970-01-03 +\. + +CREATE FOREIGN TABLE ft1 ( + c1 integer, + c2 text, + c3 date +) SERVER loopback1 OPTIONS (relname 't1'); + +CREATE FOREIGN TABLE ft2 ( + c1 integer, + c2 text, + c3 date +) SERVER loopback2 OPTIONS (relname 'invalid'); + +-- simple query and connection caching +SELECT ft1.* FROM ft1 ORDER BY c1; +SELECT * FROM ft2 ORDER BY c1; -- ERROR +ALTER USER MAPPING FOR PUBLIC SERVER loopback2 OPTIONS (DROP user); +SELECT * FROM ft2 ORDER BY c1; -- ERROR +ALTER FOREIGN TABLE ft2 OPTIONS (SET relname 't2'); +SELECT * FROM ft2 ORDER BY c1; + +-- subquery +SELECT c1 FROM t1 WHERE c1 = (SELECT c1 FROM ft1 ORDER BY c1 LIMIT 1); +SELECT c1 FROM ft1 WHERE c1 = (SELECT c1 FROM t1 ORDER BY c1 LIMIT 1); + +-- query with projection +SELECT c1 FROM ft1 ORDER BY c1; + +-- join two foreign tables +SELECT * FROM ft1 JOIN ft2 ON (ft1.c1 = ft2.c1) ORDER BY ft1.c1; +-- join itself +SELECT * FROM ft1 t1 JOIN ft1 t2 ON (t1.c1 = t2.c1) ORDER BY t1.c1; +-- outer join +SELECT * FROM ft1 t1 LEFT JOIN ft2 t2 ON (t1.c1 = t2.c1) ORDER BY 1,2,3,4,5,6; +-- WHERE clause push-down +SELECT * FROM ft1 WHERE c1 = 1 AND c2 = lower('FOO') AND c3 < now() and c3 < clock_timestamp(); +EXPLAIN (COSTS FALSE) SELECT * FROM ft1 WHERE c1 = 1 AND c2 = lower('FOO') AND c3 < now() and c3 < clock_timestamp(); +-- prepared statement +PREPARE st(int) AS SELECT * FROM ft1 WHERE c1 > $1 ORDER BY c1; +EXECUTE st(1); +EXECUTE st(2); +DEALLOCATE st; + +-- clean up +DROP TABLE t1 CASCADE; +DROP EXTENSION postgresql_fdw CASCADE; diff --git a/doc/src/sgml/contrib.sgml b/doc/src/sgml/contrib.sgml index adf09ca872..b60436cba1 100644 --- a/doc/src/sgml/contrib.sgml +++ b/doc/src/sgml/contrib.sgml @@ -123,6 +123,7 @@ CREATE EXTENSION <replaceable>module_name</> FROM unpackaged; &pgtestfsync; &pgtrgm; &pgupgrade; + &postgresql-fdw; &seg; &sepgsql; &contrib-spi; diff --git a/doc/src/sgml/filelist.sgml b/doc/src/sgml/filelist.sgml index ed39e0b661..6a734647a5 100644 --- a/doc/src/sgml/filelist.sgml +++ b/doc/src/sgml/filelist.sgml @@ -129,6 +129,7 @@ <!ENTITY pgtestfsync SYSTEM "pgtestfsync.sgml"> <!ENTITY pgtrgm SYSTEM "pgtrgm.sgml"> <!ENTITY pgupgrade SYSTEM "pgupgrade.sgml"> +<!ENTITY postgresql-fdw SYSTEM "postgresql-fdw.sgml"> <!ENTITY seg SYSTEM "seg.sgml"> <!ENTITY contrib-spi SYSTEM "contrib-spi.sgml"> <!ENTITY sepgsql SYSTEM "sepgsql.sgml"> diff --git a/doc/src/sgml/postgresql-fdw.sgml b/doc/src/sgml/postgresql-fdw.sgml new file mode 100644 index 0000000000..a368acbdae --- /dev/null +++ b/doc/src/sgml/postgresql-fdw.sgml @@ -0,0 +1,346 @@ +<!-- doc/src/sgml/postgresql_fdw.sgml --> + +<sect1 id="postgresql-fdw"> + <title>postgresql_fdw</title> + + <indexterm zone="postgresql-fdw"> + <primary>postgresql_fdw</primary> + </indexterm> + + <para> + The <filename>postgresql_fdw</> module provides foreign-data wrapper + handler function <function>postgresql_fdw_handler</function> which can be + used to access external <productname>PostgreSQL</> server via plain SQL. + </para> + + <sect2> + <title>Options</title> + + <sect3> + <title>Connection options</title> + <para> + <filename>postgresql_fdw</> retrieves connection information from options + of foreign server and user mapping. + <literal>user</literal> and <literal>password</literal> can be + specified only for user mapping, and other options can be + specified for only foreign server. The <filename> postgresql_fdw</> + accepts all libpq connection options. + Connection information which was not specified on FDW objects is determined + with libpq's rule. + See <xref linkend="sql-createforeigndatawrapper">, + <xref linkend="sql-createserver">, + <xref linkend="sql-createusermapping"> for details of FDW object + definition. + See <xref linkend="libpq-connect"> for details of libpq connection + information. + </para> + </sect3> + + <sect3> + <title>Object name options</title> + <para> + <filename>postgresql_fdw</> accepts <literal>nspname</literal> + and <literal>relname</literal> options which can be used to specify + object name used in the remote query. The <literal>nspname</literal> + is used as schema name, and <literal>relname</literal> is used as + relation name. + These options are retrieved from foreign table's definition. + If these options are omitted , local catalog names are used. + See <xref linkend="sql-createforeigntable"> for details of defining foreign + table. + </para> + </sect3> + + </sect2> + + <sect2> + <title>Examples</title> + + <para> +It is assumed that tables and data are prepared on the remote side. +<programlisting> +CREATE USER pgfdw_remote WITH PASSWORD 'secret'; +SET ROLE pgfdw_remote; + +-- definition of external data +CREATE TABLE films ( + code char(5) CONSTRAINT firstkey PRIMARY KEY, + title varchar(40) NOT NULL, + did integer NOT NULL, + date_prod date, + kind varchar(10), + len interval hour to minute +); + +INSERT INTO films VALUES + ('UA502', 'Bananas', 105, '1971-07-13', 'Comedy', '82 minutes'), + ('T_601', 'Yojimbo', 106, '1961-06-16', 'Drama', NULL), + ('B6717', 'Tampopo', 110, '1985-02-10', 'Comedy', NULL), + ('HG120', 'The Dinner Game', 140, DEFAULT, 'Comedy', NULL); +</programlisting> + </para> + + <para> + On the local side, you need to build and install <filename>postgresql_fdw</> + binary first. Then you can create foreign-data wrapper + <filename>postgresql_fdw</> by executing + <xref linkend="sql-createextension">. + After creating foreign-data wrapper, you need to create user +<programlisting> +-- this creates foreign-data wrapper postgresql_fdw automatically +CREATE EXTENSION postgresql_fdw; + +-- store connection information per server +CREATE SERVER myserver FOREIGN DATA WRAPPER postgresql_fdw + OPTIONS (host 'foo', dbname 'foodb', port '5432'); + +-- map local user to remote user +CREATE USER MAPPING FOR pgfdw_local SERVER myserver + OPTIONS (user 'pgfdw_remote', password 'secret'); + +-- define form of remote data as foreign table +CREATE FOREIGN TABLE external_table_films ( + code char(5) NOT NULL, + title varchar(40) NOT NULL, + did integer NOT NULL, + date_prod date, + kind varchar(10), + len interval hour to minute +) SERVER myserver OPTIONS (relname 'films'); + +-- now you can retrieve data from remote PostgreSQL server +SELECT * FROM external_table_films; + code | title | did | date_prod | kind | len +-------+-----------------+-----+------------+--------+---------- + UA502 | Bananas | 105 | 1971-07-13 | Comedy | 01:22:00 + T_601 | Yojimbo | 106 | 1961-06-16 | Drama | + B6717 | Tampopo | 110 | 1985-02-10 | Comedy | + HG120 | The Dinner Game | 140 | | Comedy | +(4 rows) +</programlisting> + </para> + + </sect2> + + <sect2> + <title>Function</title> + + <variablelist> + <varlistentry> + <term> + <function>postgresql_fdw_handler() returns fdw_handler</function> + </term> + + <listitem> + <para> + <function>postgresql_fdw_handler</function> is a foreign-data wrapper + handler function which returns foreign-data wrapper handler for + <productname>PostgreSQL</> in type of <type>fdw_handler</type>. + Since <type>fdw_handler</type> is a pseudo type, <function> + postgresql_fdw_handler</function> can't be called from a SQL statement. + </para> + </listitem> + </varlistentry> + </variablelist> + + </sect2> + + <sect2> + <title>Notes</title> + + <para> + <itemizedlist> + <listitem> + <para> + The <filename>postgresql_fdw</> connects to a remote <productname> + PostgreSQL</> server when a scan on the server is requested first + time in the local query. The connection is used by all of remote + queries which are executed on same remote <productname>PostgreSQL</> + server. If the local query uses multiple foreign <productname> + PostgreSQL</> servers, connections are established for each server + (not for each foreign table) and all of them will be closed at the + end of the query. This also means that connection pooling is not + implemented in <filename>postgresql_fdw</>. + </para> + </listitem> + <listitem> + <para> + The <filename>postgresql_fdw</> never emit transaction command such + as <command>BEGIN</>, <command>ROLLBACK</> and <command>COMMIT</>. + Thus, all SQL statements are executed in each transaction when + <varname>autocommit</> was set to 'on'. + </para> + </listitem> + <listitem> + <para> + The <filename>postgresql_fdw</> retrieves all of the result tuples + at once via libpq when the query was executed. Note that huge result + set causes huge memory consumption. The memory for the result set + will be freed at the end of the each query. + </para> + </listitem> + + <listitem> + <para> + <xref linkend="sql-explain"> shows a local plan for the query. + "Remote SQL" of ForeignScan node is the query which will be send to the + remote server. + </para> + </listitem> + <listitem> + <para> + The <filename>postgresql_fdw</> pushes some part of <command>WHERE</> + clause down to the remote server, only if the evaluating the part + of clause doesn't break the consistency of the query. If a clause + consist of elements below, the clause will be pushed down. + </para> + <table id="postgresql-fdw-push-downable"> + <title>push-down-able elements</title> + <tgroup cols="2"> + <thead> + <row> + <entry>Element</entry> + <entry>Note</entry> + </row> + </thead> + <tbody> + <row> + <entry>Constant value and column reference</entry> + <entry></entry> + </row> + <row> + <entry>Array of push-down-able type</entry> + <entry></entry> + </row> + <row> + <entry>Parameter of <command>EXECUTE</command></entry> + <entry></entry> + </row> + <row> + <entry>Bool expression such as <literal>A AND B</literal> or + <literal>A OR B</literal></entry> + <entry></entry> + </row> + <row> + <entry>Non-volatile operator</entry> + <entry></entry> + </row> + <row> + <entry><command>DISTINCT</command>> operator, such as + <literal>A IS DISTINCT FROM B</literal></entry> + <entry></entry> + </row> + <row> + <entry>Scalar array operator, such as <literal>ALL(...)</literal> and + <literal>ANY(...)</literal></entry> + <entry></entry> + </row> + <row> + <entry>Non-volatile function call</entry> + <entry></entry> + </row> + </tbody> + </tgroup> + </table> + <para> + "Remote query" in the result of <xref linkend="sql-explain"> shows + whether each condition in the <command>WHERE</> clause of the local + query will be send to remote side or not. + </para> + </listitem> + <listitem> + <para> + All conditions in <command>HAVING</command> clause are evaluated on the + local side because aggregate push-down has not been implemented yet. + <command>UNION</command> clause, <command>ORDER BY</command> clause and + <command>LIMIT</command> clause are also evaluated on the local side. + </para> + </listitem> + <listitem> + <para> + Functions which are used in <command>SELECT</command> clause are executed + on local side. + </para> + </listitem> + <listitem> + <para> + If you want to use a set returning function like + <function>generate_series</> as data source, you need to define a view on + the remote side and specify its name as relname option of the foreign + table. In this way, sequences and large objects on the remote side can be + handled via <filename>postgresql_fdw</> if the operation doesn't need any + parameter. + </para> + </listitem> + <listitem> + <para> + Note that the difference between local and remote could cause unexpected + results. + <itemizedlist> + <listitem> + <para> + The second part of local <varname>datestyle</>, which specifies + year/month/day ordering, should fit remote <varname>datestyle</>. + For example, if the remote <varname>datestyle</> contains + <literal>German</> as the first part, local <varname>datestyle</> + should contain <literal>dmy</> as the second part. + </para> + </listitem> + <listitem> + <para> + If settings of <varname>timezone</> are different between local and + remote, note that <type>timestamp without time zone</> values will be + treated as local timezone. + </para> + </listitem> + <listitem> + <para> + Locale parameters affect to various appearance such as messages and + currency format. + </para> + <para> + If you use <function>to_char</> function to format currency value in + <command>SELECT</> statement, local locale setting will be used + because <function>to_char</>, a stable function, will be evaluated on + the local side. If you want to use remote locale for formatting, you + would need a view which use <function>to_char</> on remote side. + </para> + </listitem> + </itemizedlist> + </para> + </listitem> + <listitem> + <para> + <filename>postgresql_fdw</> uses libpq version-3 to retrieve external + data. PostgreSQL 7.4 or higher are available as remote server. + Anyway, please ensure that functions/operators have same meaning on both + side because <filename>postgresql_fdw</> generates SQL statement based + on definitions of functions and operators on the local side. + </para> + <para> + If a function or an operator which is used in local query doesn't exist, + remote query will fail and cause error. + </para> + </listitem> + <listitem> + <para> + pg_dump dumps only definition of foreign tables, and doesn't dump + external data. If you want to dump external data on local side, use + <command>COPY</> for each foreign table. + </para> + </listitem> + + <listitem> + <para> + Foreign tables can't have OID system column. If the external table was + defined with <literal>WITH OIDS</>, add <literal>oid</> column to the + foreign table. Note that <command>SELECT * FROM foreign_table</> will + return oid always in such case. + </para> + </listitem> + </itemizedlist> + </para> + + </sect2> + +</sect1> diff --git a/src/backend/foreign/foreign.c b/src/backend/foreign/foreign.c index 4a7b2c30cf..54d33eedb3 100644 --- a/src/backend/foreign/foreign.c +++ b/src/backend/foreign/foreign.c @@ -402,10 +402,11 @@ pg_options_to_table(PG_FUNCTION_ARGS) /* * Describes the valid options for postgresql FDW, server, and user mapping. */ -struct ConnectionOption +struct FdwOption { - const char *optname; + const char *optname; /* name of the option */ Oid optcontext; /* Oid of catalog in which option may appear */ + bool is_conninfo; /* T if the option is a libpq conninfo option */ }; /* @@ -413,22 +414,26 @@ struct ConnectionOption * * The list is small - don't bother with bsearch if it stays so. */ -static struct ConnectionOption libpq_conninfo_options[] = { - {"authtype", ForeignServerRelationId}, - {"service", ForeignServerRelationId}, - {"user", UserMappingRelationId}, - {"password", UserMappingRelationId}, - {"connect_timeout", ForeignServerRelationId}, - {"dbname", ForeignServerRelationId}, - {"host", ForeignServerRelationId}, - {"hostaddr", ForeignServerRelationId}, - {"port", ForeignServerRelationId}, - {"tty", ForeignServerRelationId}, - {"options", ForeignServerRelationId}, - {"requiressl", ForeignServerRelationId}, - {"sslmode", ForeignServerRelationId}, - {"gsslib", ForeignServerRelationId}, - {NULL, InvalidOid} +static struct FdwOption postgresql_fdw_options[] = { + /* libpq connection options */ + {"authtype", ForeignServerRelationId, true}, + {"service", ForeignServerRelationId, true}, + {"user", UserMappingRelationId, true}, + {"password", UserMappingRelationId, true}, + {"connect_timeout", ForeignServerRelationId, true}, + {"dbname", ForeignServerRelationId, true}, + {"host", ForeignServerRelationId, true}, + {"hostaddr", ForeignServerRelationId, true}, + {"port", ForeignServerRelationId, true}, + {"tty", ForeignServerRelationId, true}, + {"options", ForeignServerRelationId, true}, + {"requiressl", ForeignServerRelationId, true}, + {"sslmode", ForeignServerRelationId, true}, + {"gsslib", ForeignServerRelationId, true}, + /* catalog options */ + {"nspname", ForeignTableRelationId, false}, + {"relname", ForeignTableRelationId, false}, + {NULL, InvalidOid, false} }; @@ -437,12 +442,28 @@ static struct ConnectionOption libpq_conninfo_options[] = { * context is the Oid of the catalog the option came from, or 0 if we * don't care. */ +bool +is_conninfo_option(const char *option) +{ + struct FdwOption *opt; + + for (opt = postgresql_fdw_options; opt->optname; opt++) + if (strcmp(opt->optname, option) == 0) + return true; + return false; +} + +/* + * Check if the provided option is one of postgresql_fdw options. + * context is the Oid of the catalog the option came from, or 0 if we + * don't care. + */ static bool -is_conninfo_option(const char *option, Oid context) +is_postgresql_fdw_option(const char *option, Oid context) { - struct ConnectionOption *opt; + struct FdwOption *opt; - for (opt = libpq_conninfo_options; opt->optname; opt++) + for (opt = postgresql_fdw_options; opt->optname; opt++) if (context == opt->optcontext && strcmp(opt->optname, option) == 0) return true; return false; @@ -469,9 +490,9 @@ postgresql_fdw_validator(PG_FUNCTION_ARGS) { DefElem *def = lfirst(cell); - if (!is_conninfo_option(def->defname, catalog)) + if (!is_postgresql_fdw_option(def->defname, catalog)) { - struct ConnectionOption *opt; + struct FdwOption *opt; StringInfoData buf; /* @@ -479,7 +500,7 @@ postgresql_fdw_validator(PG_FUNCTION_ARGS) * with list of valid options for the object. */ initStringInfo(&buf); - for (opt = libpq_conninfo_options; opt->optname; opt++) + for (opt = postgresql_fdw_options; opt->optname; opt++) if (catalog == opt->optcontext) appendStringInfo(&buf, "%s%s", (buf.len > 0) ? ", " : "", opt->optname); diff --git a/src/include/foreign/foreign.h b/src/include/foreign/foreign.h index 2c436aef80..094ac58d79 100644 --- a/src/include/foreign/foreign.h +++ b/src/include/foreign/foreign.h @@ -75,6 +75,7 @@ extern ForeignDataWrapper *GetForeignDataWrapper(Oid fdwid); extern ForeignDataWrapper *GetForeignDataWrapperByName(const char *name, bool missing_ok); extern ForeignTable *GetForeignTable(Oid relid); +bool is_conninfo_option(const char *option); extern Oid get_foreign_data_wrapper_oid(const char *fdwname, bool missing_ok); extern Oid get_foreign_server_oid(const char *servername, bool missing_ok); |